summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKamil Trzcinski <ayufan@ayufan.eu>2016-12-18 23:39:51 +0100
committerKamil Trzcinski <ayufan@ayufan.eu>2016-12-18 23:39:51 +0100
commit14d47884dff6844625c2e65b247fd773d78f5ea2 (patch)
tree10fba027e61521df236fd6eec7ba829c5fe2c8ec
parent9fd775def2d89500cf291fe675458b68ead7cd2c (diff)
parent546fa165ff728bc2d25ed9b55b95dd1d48139d4a (diff)
downloadgitlab-ce-dockerfile-templates.tar.gz
Merge remote-tracking branch 'origin/master' into dockerfile-templatesdockerfile-templates
-rw-r--r--.eslintignore2
-rw-r--r--.eslintrc33
-rw-r--r--.gitattributes1
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml175
-rw-r--r--.gitlab/issue_templates/Bug.md2
-rw-r--r--.rubocop.yml3
-rw-r--r--.ruby-version2
-rw-r--r--.scss-lint.yml2
-rw-r--r--CHANGELOG.md272
-rw-r--r--CONTRIBUTING.md11
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile28
-rw-r--r--Gemfile.lock131
-rw-r--r--README.md8
-rw-r--r--VERSION2
-rw-r--r--app/assets/fonts/OFL.txt93
-rw-r--r--app/assets/fonts/SourceSansPro-Black.ttf.woffbin113800 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Black.ttf.woff2bin82052 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BlackIt.ttf.woffbin49704 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2bin34812 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Bold.ttf.woffbin117872 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Bold.ttf.woff2bin85604 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BoldIt.ttf.woffbin50608 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2bin35864 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLight.ttf.woffbin114336 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2bin82808 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woffbin49684 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2bin34560 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-It.ttf.woffbin51012 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-It.ttf.woff2bin36016 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Light.ttf.woffbin118284 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Light.ttf.woff2bin86336 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-LightIt.ttf.woffbin50992 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-LightIt.ttf.woff2bin35952 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Regular.ttf.woffbin119064 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Regular.ttf.woff2bin86844 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Semibold.ttf.woffbin118412 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Semibold.ttf.woff2bin86196 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woffbin50924 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2bin35984 -> 0 bytes
-rw-r--r--app/assets/javascripts/abuse_reports.js.es63
-rw-r--r--app/assets/javascripts/activities.js37
-rw-r--r--app/assets/javascripts/activities.js.es637
-rw-r--r--app/assets/javascripts/admin.js4
-rw-r--r--app/assets/javascripts/api.js6
-rw-r--r--app/assets/javascripts/application.js47
-rw-r--r--app/assets/javascripts/aside.js2
-rw-r--r--app/assets/javascripts/autosave.js2
-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/details_behavior.js2
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js3
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js2
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js2
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js.es64
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js4
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.js3
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selectors.js4
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js3
-rw-r--r--app/assets/javascripts/blob/blob_license_selectors.js.es64
-rw-r--r--app/assets/javascripts/blob/template_selector.js.es65
-rw-r--r--app/assets/javascripts/blob_edit/blob_edit_bundle.js5
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js7
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es617
-rw-r--r--app/assets/javascripts/boards/components/board.js.es620
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.js.es67
-rw-r--r--app/assets/javascripts/boards/components/board_card.js.es611
-rw-r--r--app/assets/javascripts/boards/components/board_delete.js.es64
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es642
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.js.es623
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js.es612
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js.es6102
-rw-r--r--app/assets/javascripts/boards/filters/due_date_filters.js.es63
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js.es66
-rw-r--r--app/assets/javascripts/boards/models/issue.js.es67
-rw-r--r--app/assets/javascripts/boards/models/label.js.es63
-rw-r--r--app/assets/javascripts/boards/models/list.js.es616
-rw-r--r--app/assets/javascripts/boards/models/milestone.js.es65
-rw-r--r--app/assets/javascripts/boards/models/user.js.es65
-rw-r--r--app/assets/javascripts/boards/services/board_service.js.es64
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js.es613
-rw-r--r--app/assets/javascripts/boards/test_utils/simulate_drag.js2
-rw-r--r--app/assets/javascripts/boards/vue_resource_interceptor.js.es64
-rw-r--r--app/assets/javascripts/breakpoints.js6
-rw-r--r--app/assets/javascripts/broadcast_message.js2
-rw-r--r--app/assets/javascripts/build.js107
-rw-r--r--app/assets/javascripts/build_artifacts.js2
-rw-r--r--app/assets/javascripts/build_variables.js.es63
-rw-r--r--app/assets/javascripts/commit.js4
-rw-r--r--app/assets/javascripts/commit/file.js6
-rw-r--r--app/assets/javascripts/commit/image_file.js4
-rw-r--r--app/assets/javascripts/commits.js12
-rw-r--r--app/assets/javascripts/compare.js5
-rw-r--r--app/assets/javascripts/compare_autocomplete.js.es63
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js2
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js7
-rw-r--r--app/assets/javascripts/create_label.js.es64
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es645
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es647
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es644
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es647
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es657
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es644
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es644
-rw-r--r--app/assets/javascripts/cycle_analytics/components/total_time_component.js.es625
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6217
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es641
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es694
-rw-r--r--app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es67
-rw-r--r--app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es67
-rw-r--r--app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es67
-rw-r--r--app/assets/javascripts/diff.js77
-rw-r--r--app/assets/javascripts/diff.js.es6112
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es619
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es68
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js.es621
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js.es69
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es614
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js.es648
-rw-r--r--app/assets/javascripts/diff_notes/mixins/discussion.js.es63
-rw-r--r--app/assets/javascripts/diff_notes/models/discussion.js.es616
-rw-r--r--app/assets/javascripts/diff_notes/models/note.js.es65
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js.es66
-rw-r--r--app/assets/javascripts/diff_notes/stores/comments.js.es65
-rw-r--r--app/assets/javascripts/dispatcher.js.es686
-rw-r--r--app/assets/javascripts/dropzone_input.js3
-rw-r--r--app/assets/javascripts/due_date_select.js.es625
-rw-r--r--app/assets/javascripts/environments/components/environment.js.es6254
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.js.es649
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.js.es622
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js.es6521
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.js.es632
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.js.es626
-rw-r--r--app/assets/javascripts/environments/environments_bundle.js.es621
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js.es622
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js.es6131
-rw-r--r--app/assets/javascripts/environments/vue_resource_interceptor.js.es612
-rw-r--r--app/assets/javascripts/extensions/array.js8
-rw-r--r--app/assets/javascripts/extensions/array.js.es624
-rw-r--r--app/assets/javascripts/extensions/element.js.es619
-rw-r--r--app/assets/javascripts/extensions/jquery.js2
-rw-r--r--app/assets/javascripts/extensions/object.js.es626
-rw-r--r--app/assets/javascripts/files_comment_button.js4
-rw-r--r--app/assets/javascripts/flash.js2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es6300
-rw-r--r--app/assets/javascripts/gl_dropdown.js40
-rw-r--r--app/assets/javascripts/gl_field_errors.js.es62
-rw-r--r--app/assets/javascripts/gl_form.js8
-rw-r--r--app/assets/javascripts/graphs/graphs_bundle.js2
-rw-r--r--app/assets/javascripts/graphs/stat_graph.js2
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors.js7
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js4
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_util.js13
-rw-r--r--app/assets/javascripts/group_avatar.js2
-rw-r--r--app/assets/javascripts/group_label_subscription.js.es654
-rw-r--r--app/assets/javascripts/groups_select.js6
-rw-r--r--app/assets/javascripts/header.js2
-rw-r--r--app/assets/javascripts/importer_status.js7
-rw-r--r--app/assets/javascripts/issuable.js.es69
-rw-r--r--app/assets/javascripts/issuable_context.js4
-rw-r--r--app/assets/javascripts/issuable_form.js9
-rw-r--r--app/assets/javascripts/issue.js3
-rw-r--r--app/assets/javascripts/issue_status_select.js2
-rw-r--r--app/assets/javascripts/issues_bulk_assignment.js.es697
-rw-r--r--app/assets/javascripts/label_manager.js.es65
-rw-r--r--app/assets/javascripts/labels.js2
-rw-r--r--app/assets/javascripts/labels_select.js151
-rw-r--r--app/assets/javascripts/layout_nav.js2
-rw-r--r--app/assets/javascripts/lib/ace.js1
-rw-r--r--app/assets/javascripts/lib/chart.js2
-rw-r--r--app/assets/javascripts/lib/cropper.js2
-rw-r--r--app/assets/javascripts/lib/d3.js2
-rw-r--r--app/assets/javascripts/lib/raphael.js2
-rw-r--r--app/assets/javascripts/lib/utils/animate.js2
-rw-r--r--app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6113
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js100
-rw-r--r--app/assets/javascripts/lib/utils/custom_event_polyfill.js.es612
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js107
-rw-r--r--app/assets/javascripts/lib/utils/notify.js3
-rw-r--r--app/assets/javascripts/lib/utils/pretty_time.js.es667
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js5
-rw-r--r--app/assets/javascripts/lib/utils/type_utility.js2
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js2
-rw-r--r--app/assets/javascripts/line_highlighter.js3
-rw-r--r--app/assets/javascripts/logo.js4
-rw-r--r--app/assets/javascripts/member_expiration_date.js2
-rw-r--r--app/assets/javascripts/members.js.es665
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es68
-rw-r--r--app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es64
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es615
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es624
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es63
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es65
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es68
-rw-r--r--app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es63
-rw-r--r--app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es63
-rw-r--r--app/assets/javascripts/merge_request.js26
-rw-r--r--app/assets/javascripts/merge_request_tabs.js441
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.es6389
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es6107
-rw-r--r--app/assets/javascripts/merged_buttons.js3
-rw-r--r--app/assets/javascripts/milestone.js4
-rw-r--r--app/assets/javascripts/milestone_select.js8
-rw-r--r--app/assets/javascripts/namespace_select.js10
-rw-r--r--app/assets/javascripts/network/branch_graph.js4
-rw-r--r--app/assets/javascripts/network/network.js4
-rw-r--r--app/assets/javascripts/network/network_bundle.js5
-rw-r--r--app/assets/javascripts/new_branch_form.js2
-rw-r--r--app/assets/javascripts/new_commit_form.js2
-rw-r--r--app/assets/javascripts/notes.js95
-rw-r--r--app/assets/javascripts/notifications_dropdown.js4
-rw-r--r--app/assets/javascripts/notifications_form.js2
-rw-r--r--app/assets/javascripts/pager.js64
-rw-r--r--app/assets/javascripts/pager.js.es673
-rw-r--r--app/assets/javascripts/pipelines.js.es631
-rw-r--r--app/assets/javascripts/preview_markdown.js9
-rw-r--r--app/assets/javascripts/profile/gl_crop.js.es63
-rw-r--r--app/assets/javascripts/profile/profile.js.es65
-rw-r--r--app/assets/javascripts/profile/profile_bundle.js2
-rw-r--r--app/assets/javascripts/project.js6
-rw-r--r--app/assets/javascripts/project_avatar.js2
-rw-r--r--app/assets/javascripts/project_find_file.js4
-rw-r--r--app/assets/javascripts/project_fork.js2
-rw-r--r--app/assets/javascripts/project_import.js4
-rw-r--r--app/assets/javascripts/project_label_subscription.js.es654
-rw-r--r--app/assets/javascripts/project_new.js2
-rw-r--r--app/assets/javascripts/project_select.js4
-rw-r--r--app/assets/javascripts/project_show.js2
-rw-r--r--app/assets/javascripts/project_variables.js.es643
-rw-r--r--app/assets/javascripts/projects_list.js7
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es63
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js.es64
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es63
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js.es66
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es63
-rw-r--r--app/assets/javascripts/protected_branches/protected_branches_bundle.js1
-rw-r--r--app/assets/javascripts/render_gfm.js16
-rw-r--r--app/assets/javascripts/render_math.js55
-rw-r--r--app/assets/javascripts/right_sidebar.js4
-rw-r--r--app/assets/javascripts/search.js4
-rw-r--r--app/assets/javascripts/search_autocomplete.js.es63
-rw-r--r--app/assets/javascripts/shortcuts.js6
-rw-r--r--app/assets/javascripts/shortcuts_blob.js4
-rw-r--r--app/assets/javascripts/shortcuts_dashboard_navigation.js4
-rw-r--r--app/assets/javascripts/shortcuts_find_file.js4
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js6
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js4
-rw-r--r--app/assets/javascripts/shortcuts_network.js4
-rw-r--r--app/assets/javascripts/sidebar.js.es64
-rw-r--r--app/assets/javascripts/signin_tabs_memoizer.js.es649
-rw-r--r--app/assets/javascripts/single_file_diff.js24
-rw-r--r--app/assets/javascripts/smart_interval.js.es6157
-rw-r--r--app/assets/javascripts/snippet/snippet_bundle.js4
-rw-r--r--app/assets/javascripts/snippets_list.js.es63
-rw-r--r--app/assets/javascripts/star.js4
-rw-r--r--app/assets/javascripts/subbable_resource.js.es654
-rw-r--r--app/assets/javascripts/subscription.js52
-rw-r--r--app/assets/javascripts/subscription.js.es650
-rw-r--r--app/assets/javascripts/subscription_select.js2
-rw-r--r--app/assets/javascripts/syntax_highlight.js9
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js.es64
-rw-r--r--app/assets/javascripts/templates/issuable_template_selectors.js.es63
-rw-r--r--app/assets/javascripts/todos.js.es67
-rw-r--r--app/assets/javascripts/tree.js3
-rw-r--r--app/assets/javascripts/u2f/authenticate.js6
-rw-r--r--app/assets/javascripts/u2f/error.js4
-rw-r--r--app/assets/javascripts/u2f/register.js6
-rw-r--r--app/assets/javascripts/u2f/util.js2
-rw-r--r--app/assets/javascripts/user.js.es64
-rw-r--r--app/assets/javascripts/user_tabs.js.es65
-rw-r--r--app/assets/javascripts/username_validator.js.es65
-rw-r--r--app/assets/javascripts/users/calendar.js6
-rw-r--r--app/assets/javascripts/users/users_bundle.js2
-rw-r--r--app/assets/javascripts/users_select.js6
-rw-r--r--app/assets/javascripts/vue_common_component/commit.js.es6163
-rw-r--r--app/assets/javascripts/wikis.js38
-rw-r--r--app/assets/javascripts/wikis.js.es673
-rw-r--r--app/assets/javascripts/zen_mode.js5
-rw-r--r--app/assets/stylesheets/framework.scss10
-rw-r--r--app/assets/stylesheets/framework/asciidoctor.scss27
-rw-r--r--app/assets/stylesheets/framework/avatar.scss10
-rw-r--r--app/assets/stylesheets/framework/awards.scss141
-rw-r--r--app/assets/stylesheets/framework/blank.scss4
-rw-r--r--app/assets/stylesheets/framework/blocks.scss46
-rw-r--r--app/assets/stylesheets/framework/broadcast-messages.scss21
-rw-r--r--app/assets/stylesheets/framework/buttons.scss75
-rw-r--r--app/assets/stylesheets/framework/calendar.scss6
-rw-r--r--app/assets/stylesheets/framework/callout.scss26
-rw-r--r--app/assets/stylesheets/framework/common.scss99
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss120
-rw-r--r--app/assets/stylesheets/framework/emojis.scss1809
-rw-r--r--app/assets/stylesheets/framework/files.scss22
-rw-r--r--app/assets/stylesheets/framework/flash.scss2
-rw-r--r--app/assets/stylesheets/framework/fonts.scss45
-rw-r--r--app/assets/stylesheets/framework/forms.scss57
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss33
-rw-r--r--app/assets/stylesheets/framework/header.scss47
-rw-r--r--app/assets/stylesheets/framework/highlight.scss4
-rw-r--r--app/assets/stylesheets/framework/icons.scss51
-rw-r--r--app/assets/stylesheets/framework/images.scss11
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss8
-rw-r--r--app/assets/stylesheets/framework/jquery.scss16
-rw-r--r--app/assets/stylesheets/framework/layout.scss41
-rw-r--r--app/assets/stylesheets/framework/lists.scss61
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss44
-rw-r--r--app/assets/stylesheets/framework/mixins.scss6
-rw-r--r--app/assets/stylesheets/framework/mobile.scss8
-rw-r--r--app/assets/stylesheets/framework/nav.scss92
-rw-r--r--app/assets/stylesheets/framework/page-header.scss67
-rw-r--r--app/assets/stylesheets/framework/pagination.scss4
-rw-r--r--app/assets/stylesheets/framework/selects.scss20
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss12
-rw-r--r--app/assets/stylesheets/framework/snippets.scss48
-rw-r--r--app/assets/stylesheets/framework/tables.scss25
-rw-r--r--app/assets/stylesheets/framework/timeline.scss2
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss10
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss28
-rw-r--r--app/assets/stylesheets/framework/typography.scss21
-rw-r--r--app/assets/stylesheets/framework/variables.scss408
-rw-r--r--app/assets/stylesheets/framework/wells.scss58
-rw-r--r--app/assets/stylesheets/framework/zen.scss10
-rw-r--r--app/assets/stylesheets/highlight/dark.scss230
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss225
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss243
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss243
-rw-r--r--app/assets/stylesheets/highlight/white.scss187
-rw-r--r--app/assets/stylesheets/mailers/devise.scss4
-rw-r--r--app/assets/stylesheets/mailers/highlighted_diff_email.scss207
-rw-r--r--app/assets/stylesheets/mailers/repository_push_email.scss143
-rw-r--r--app/assets/stylesheets/notify.scss10
-rw-r--r--app/assets/stylesheets/pages/admin.scss162
-rw-r--r--app/assets/stylesheets/pages/appearances.scss11
-rw-r--r--app/assets/stylesheets/pages/awards.scss140
-rw-r--r--app/assets/stylesheets/pages/boards.scss21
-rw-r--r--app/assets/stylesheets/pages/builds.scss62
-rw-r--r--app/assets/stylesheets/pages/ci_projects.scss2
-rw-r--r--app/assets/stylesheets/pages/commit.scss150
-rw-r--r--app/assets/stylesheets/pages/commits.scss26
-rw-r--r--app/assets/stylesheets/pages/confirmation.scss32
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss369
-rw-r--r--app/assets/stylesheets/pages/dashboard.scss43
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss6
-rw-r--r--app/assets/stylesheets/pages/diff.scss69
-rw-r--r--app/assets/stylesheets/pages/editor.scss57
-rw-r--r--app/assets/stylesheets/pages/emojis.scss1809
-rw-r--r--app/assets/stylesheets/pages/environments.scss86
-rw-r--r--app/assets/stylesheets/pages/errors.scss16
-rw-r--r--app/assets/stylesheets/pages/events.scss11
-rw-r--r--app/assets/stylesheets/pages/explore.scss8
-rw-r--r--app/assets/stylesheets/pages/graph.scss13
-rw-r--r--app/assets/stylesheets/pages/groups.scss31
-rw-r--r--app/assets/stylesheets/pages/help.scss10
-rw-r--r--app/assets/stylesheets/pages/icons.scss12
-rw-r--r--app/assets/stylesheets/pages/issuable.scss93
-rw-r--r--app/assets/stylesheets/pages/issues.scss37
-rw-r--r--app/assets/stylesheets/pages/labels.scss19
-rw-r--r--app/assets/stylesheets/pages/lint.scss8
-rw-r--r--app/assets/stylesheets/pages/login.scss30
-rw-r--r--app/assets/stylesheets/pages/members.scss35
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss9
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss59
-rw-r--r--app/assets/stylesheets/pages/milestone.scss108
-rw-r--r--app/assets/stylesheets/pages/note_form.scss10
-rw-r--r--app/assets/stylesheets/pages/notes.scss178
-rw-r--r--app/assets/stylesheets/pages/notifications.scss18
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss613
-rw-r--r--app/assets/stylesheets/pages/profile.scss22
-rw-r--r--app/assets/stylesheets/pages/projects.scss118
-rw-r--r--app/assets/stylesheets/pages/runners.scss12
-rw-r--r--app/assets/stylesheets/pages/search.scss20
-rw-r--r--app/assets/stylesheets/pages/settings.scss6
-rw-r--r--app/assets/stylesheets/pages/snippets.scss42
-rw-r--r--app/assets/stylesheets/pages/stat_graph.scss16
-rw-r--r--app/assets/stylesheets/pages/status.scss102
-rw-r--r--app/assets/stylesheets/pages/tags.scss7
-rw-r--r--app/assets/stylesheets/pages/todos.scss10
-rw-r--r--app/assets/stylesheets/pages/tree.scss12
-rw-r--r--app/assets/stylesheets/pages/ui_dev_kit.scss4
-rw-r--r--app/assets/stylesheets/pages/votes.scss4
-rw-r--r--app/assets/stylesheets/pages/wiki.scss125
-rw-r--r--app/assets/stylesheets/pages/xterm.scss774
-rw-r--r--app/controllers/admin/application_settings_controller.rb6
-rw-r--r--app/controllers/admin/applications_controller.rb5
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/application_controller.rb12
-rw-r--r--app/controllers/autocomplete_controller.rb10
-rw-r--r--app/controllers/concerns/creates_commit.rb6
-rw-r--r--app/controllers/concerns/cycle_analytics_params.rb7
-rw-r--r--app/controllers/concerns/diff_for_path.rb2
-rw-r--r--app/controllers/concerns/issuable_actions.rb2
-rw-r--r--app/controllers/concerns/issuable_collections.rb4
-rw-r--r--app/controllers/concerns/issues_action.rb1
-rw-r--r--app/controllers/concerns/lfs_request.rb109
-rw-r--r--app/controllers/concerns/merge_requests_action.rb8
-rw-r--r--app/controllers/concerns/oauth_applications.rb19
-rw-r--r--app/controllers/concerns/service_params.rb2
-rw-r--r--app/controllers/concerns/toggle_award_emoji.rb5
-rw-r--r--app/controllers/concerns/toggle_subscription_action.rb6
-rw-r--r--app/controllers/concerns/workhorse_request.rb13
-rw-r--r--app/controllers/groups/application_controller.rb2
-rw-r--r--app/controllers/groups/group_members_controller.rb12
-rw-r--r--app/controllers/groups/labels_controller.rb7
-rw-r--r--app/controllers/groups/milestones_controller.rb4
-rw-r--r--app/controllers/help_controller.rb8
-rw-r--r--app/controllers/import/bitbucket_controller.rb83
-rw-r--r--app/controllers/jwt_controller.rb6
-rw-r--r--app/controllers/oauth/applications_controller.rb2
-rw-r--r--app/controllers/profiles/avatars_controller.rb1
-rw-r--r--app/controllers/profiles/chat_names_controller.rb64
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb12
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb8
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb48
-rw-r--r--app/controllers/projects/avatars_controller.rb1
-rw-r--r--app/controllers/projects/blob_controller.rb20
-rw-r--r--app/controllers/projects/branches_controller.rb11
-rw-r--r--app/controllers/projects/commit_controller.rb16
-rw-r--r--app/controllers/projects/commits_controller.rb2
-rw-r--r--app/controllers/projects/compare_controller.rb2
-rw-r--r--app/controllers/projects/cycle_analytics/events_controller.rb65
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb47
-rw-r--r--app/controllers/projects/discussions_controller.rb6
-rw-r--r--app/controllers/projects/environments_controller.rb15
-rw-r--r--app/controllers/projects/forks_controller.rb1
-rw-r--r--app/controllers/projects/git_http_client_controller.rb24
-rw-r--r--app/controllers/projects/git_http_controller.rb18
-rw-r--r--app/controllers/projects/issues_controller.rb17
-rw-r--r--app/controllers/projects/labels_controller.rb7
-rw-r--r--app/controllers/projects/lfs_api_controller.rb21
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb7
-rw-r--r--app/controllers/projects/merge_requests_controller.rb48
-rw-r--r--app/controllers/projects/milestones_controller.rb2
-rw-r--r--app/controllers/projects/notes_controller.rb28
-rw-r--r--app/controllers/projects/pipelines_controller.rb14
-rw-r--r--app/controllers/projects/pipelines_settings_controller.rb2
-rw-r--r--app/controllers/projects/project_members_controller.rb32
-rw-r--r--app/controllers/projects/releases_controller.rb9
-rw-r--r--app/controllers/projects/services_controller.rb7
-rw-r--r--app/controllers/projects/snippets_controller.rb8
-rw-r--r--app/controllers/projects/todos_controller.rb10
-rw-r--r--app/controllers/projects/wikis_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb85
-rw-r--r--app/controllers/registrations_controller.rb5
-rw-r--r--app/controllers/search_controller.rb2
-rw-r--r--app/controllers/sent_notifications_controller.rb2
-rw-r--r--app/controllers/sessions_controller.rb10
-rw-r--r--app/controllers/users_controller.rb5
-rw-r--r--app/finders/issuable_finder.rb94
-rw-r--r--app/finders/issues_finder.rb18
-rw-r--r--app/finders/labels_finder.rb47
-rw-r--r--app/finders/merge_requests_finder.rb1
-rw-r--r--app/finders/notes_finder.rb113
-rw-r--r--app/finders/snippets_finder.rb47
-rw-r--r--app/helpers/application_helper.rb18
-rw-r--r--app/helpers/application_settings_helper.rb22
-rw-r--r--app/helpers/auth_helper.rb2
-rw-r--r--app/helpers/builds_helper.rb10
-rw-r--r--app/helpers/button_helper.rb2
-rw-r--r--app/helpers/ci_status_helper.rb23
-rw-r--r--app/helpers/commits_helper.rb4
-rw-r--r--app/helpers/diff_helper.rb7
-rw-r--r--app/helpers/dropdowns_helper.rb2
-rw-r--r--app/helpers/environment_helper.rb31
-rw-r--r--app/helpers/environments_helper.rb7
-rw-r--r--app/helpers/events_helper.rb10
-rw-r--r--app/helpers/form_helper.rb12
-rw-r--r--app/helpers/gitlab_markdown_helper.rb2
-rw-r--r--app/helpers/gitlab_routing_helper.rb9
-rw-r--r--app/helpers/groups_helper.rb23
-rw-r--r--app/helpers/issuables_helper.rb42
-rw-r--r--app/helpers/issues_helper.rb2
-rw-r--r--app/helpers/labels_helper.rb39
-rw-r--r--app/helpers/lfs_helper.rb81
-rw-r--r--app/helpers/members_helper.rb8
-rw-r--r--app/helpers/merge_requests_helper.rb4
-rw-r--r--app/helpers/milestones_helper.rb24
-rw-r--r--app/helpers/nav_helper.rb17
-rw-r--r--app/helpers/notifications_helper.rb9
-rw-r--r--app/helpers/preferences_helper.rb6
-rw-r--r--app/helpers/projects_helper.rb27
-rw-r--r--app/helpers/search_helper.rb29
-rw-r--r--app/helpers/services_helper.rb2
-rw-r--r--app/helpers/sidekiq_helper.rb10
-rw-r--r--app/helpers/snippets_helper.rb11
-rw-r--r--app/helpers/sorting_helper.rb59
-rw-r--r--app/helpers/tab_helper.rb6
-rw-r--r--app/helpers/triggers_helper.rb12
-rw-r--r--app/mailers/emails/notes.rb2
-rw-r--r--app/mailers/emails/pipelines.rb17
-rw-r--r--app/models/application_setting.rb21
-rw-r--r--app/models/chat_name.rb12
-rw-r--r--app/models/ci/build.rb91
-rw-r--r--app/models/ci/pipeline.rb91
-rw-r--r--app/models/ci/stage.rb39
-rw-r--r--app/models/ci/variable.rb4
-rw-r--r--app/models/commit.rb75
-rw-r--r--app/models/commit_range.rb17
-rw-r--r--app/models/commit_status.rb41
-rw-r--r--app/models/concerns/has_status.rb10
-rw-r--r--app/models/concerns/issuable.rb21
-rw-r--r--app/models/concerns/mentionable.rb2
-rw-r--r--app/models/concerns/milestoneish.rb36
-rw-r--r--app/models/concerns/protected_branch_access.rb9
-rw-r--r--app/models/concerns/referable.rb13
-rw-r--r--app/models/concerns/routable.rb71
-rw-r--r--app/models/concerns/select_for_project_authorization.rb9
-rw-r--r--app/models/concerns/subscribable.rb64
-rw-r--r--app/models/concerns/token_authenticatable.rb4
-rw-r--r--app/models/cycle_analytics.rb73
-rw-r--r--app/models/cycle_analytics/summary.rb5
-rw-r--r--app/models/discussion.rb16
-rw-r--r--app/models/environment.rb74
-rw-r--r--app/models/event.rb19
-rw-r--r--app/models/global_milestone.rb33
-rw-r--r--app/models/group.rb17
-rw-r--r--app/models/guest.rb7
-rw-r--r--app/models/issue.rb127
-rw-r--r--app/models/key.rb25
-rw-r--r--app/models/label.rb15
-rw-r--r--app/models/member.rb53
-rw-r--r--app/models/merge_request.rb116
-rw-r--r--app/models/merge_request/metrics.rb1
-rw-r--r--app/models/merge_request_diff.rb16
-rw-r--r--app/models/milestone.rb38
-rw-r--r--app/models/namespace.rb54
-rw-r--r--app/models/network/graph.rb4
-rw-r--r--app/models/note.rb36
-rw-r--r--app/models/notification_setting.rb4
-rw-r--r--app/models/personal_access_token.rb2
-rw-r--r--app/models/project.rb303
-rw-r--r--app/models/project_authorization.rb8
-rw-r--r--app/models/project_feature.rb24
-rw-r--r--app/models/project_group_link.rb7
-rw-r--r--app/models/project_services/buildkite_service.rb3
-rw-r--r--app/models/project_services/chat_message/base_message.rb34
-rw-r--r--app/models/project_services/chat_message/build_message.rb82
-rw-r--r--app/models/project_services/chat_message/issue_message.rb69
-rw-r--r--app/models/project_services/chat_message/merge_message.rb60
-rw-r--r--app/models/project_services/chat_message/note_message.rb83
-rw-r--r--app/models/project_services/chat_message/pipeline_message.rb78
-rw-r--r--app/models/project_services/chat_message/push_message.rb110
-rw-r--r--app/models/project_services/chat_message/wiki_page_message.rb53
-rw-r--r--app/models/project_services/chat_notification_service.rb147
-rw-r--r--app/models/project_services/chat_service.rb21
-rw-r--r--app/models/project_services/deployment_service.rb15
-rw-r--r--app/models/project_services/drone_ci_service.rb3
-rw-r--r--app/models/project_services/emails_on_push_service.rb18
-rw-r--r--app/models/project_services/hipchat_service.rb6
-rw-r--r--app/models/project_services/irker_service.rb3
-rw-r--r--app/models/project_services/issue_tracker_service.rb4
-rw-r--r--app/models/project_services/jira_service.rb236
-rw-r--r--app/models/project_services/kubernetes_service.rb128
-rw-r--r--app/models/project_services/mattermost_notification_service.rb41
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb49
-rw-r--r--app/models/project_services/pipelines_email_service.rb20
-rw-r--r--app/models/project_services/slack_notification_service.rb40
-rw-r--r--app/models/project_services/slack_service.rb176
-rw-r--r--app/models/project_services/slack_service/base_message.rb34
-rw-r--r--app/models/project_services/slack_service/build_message.rb82
-rw-r--r--app/models/project_services/slack_service/issue_message.rb69
-rw-r--r--app/models/project_services/slack_service/merge_message.rb60
-rw-r--r--app/models/project_services/slack_service/note_message.rb83
-rw-r--r--app/models/project_services/slack_service/pipeline_message.rb79
-rw-r--r--app/models/project_services/slack_service/push_message.rb110
-rw-r--r--app/models/project_services/slack_service/wiki_page_message.rb53
-rw-r--r--app/models/project_team.rb133
-rw-r--r--app/models/project_wiki.rb2
-rw-r--r--app/models/protected_branch/merge_access_level.rb9
-rw-r--r--app/models/protected_branch/push_access_level.rb6
-rw-r--r--app/models/repository.rb470
-rw-r--r--app/models/route.rb22
-rw-r--r--app/models/service.rb12
-rw-r--r--app/models/snippet.rb17
-rw-r--r--app/models/subscription.rb7
-rw-r--r--app/models/tree.rb26
-rw-r--r--app/models/user.rb200
-rw-r--r--app/policies/ci/build_policy.rb4
-rw-r--r--app/policies/ci/pipeline_policy.rb4
-rw-r--r--app/policies/group_member_policy.rb6
-rw-r--r--app/policies/group_policy.rb6
-rw-r--r--app/policies/issue_policy.rb4
-rw-r--r--app/policies/note_policy.rb2
-rw-r--r--app/policies/personal_snippet_policy.rb5
-rw-r--r--app/policies/project_policy.rb23
-rw-r--r--app/serializers/analytics_build_entity.rb40
-rw-r--r--app/serializers/analytics_build_serializer.rb3
-rw-r--r--app/serializers/analytics_commit_entity.rb13
-rw-r--r--app/serializers/analytics_commit_serializer.rb3
-rw-r--r--app/serializers/analytics_generic_serializer.rb7
-rw-r--r--app/serializers/analytics_issue_entity.rb29
-rw-r--r--app/serializers/analytics_issue_serializer.rb3
-rw-r--r--app/serializers/analytics_merge_request_entity.rb7
-rw-r--r--app/serializers/analytics_merge_request_serializer.rb3
-rw-r--r--app/serializers/build_entity.rb19
-rw-r--r--app/serializers/commit_entity.rb7
-rw-r--r--app/serializers/deployment_entity.rb4
-rw-r--r--app/serializers/entity_date_helper.rb37
-rw-r--r--app/serializers/environment_entity.rb11
-rw-r--r--app/serializers/issuable_entity.rb16
-rw-r--r--app/serializers/issue_entity.rb9
-rw-r--r--app/serializers/issue_serializer.rb3
-rw-r--r--app/serializers/label_entity.rb11
-rw-r--r--app/serializers/merge_request_entity.rb14
-rw-r--r--app/serializers/merge_request_serializer.rb3
-rw-r--r--app/services/access_token_validation_service.rb32
-rw-r--r--app/services/after_branch_delete_service.rb21
-rw-r--r--app/services/auth/container_registry_authentication_service.rb18
-rw-r--r--app/services/chat_names/authorize_user_service.rb38
-rw-r--r--app/services/chat_names/find_user_service.rb26
-rw-r--r--app/services/ci/create_pipeline_builds_service.rb15
-rw-r--r--app/services/ci/create_pipeline_service.rb12
-rw-r--r--app/services/ci/image_for_build_service.rb8
-rw-r--r--app/services/ci/process_pipeline_service.rb26
-rw-r--r--app/services/ci/send_pipeline_notification_service.rb19
-rw-r--r--app/services/ci/stop_environments_service.rb29
-rw-r--r--app/services/commits/change_service.rb4
-rw-r--r--app/services/create_branch_service.rb2
-rw-r--r--app/services/create_deployment_service.rb2
-rw-r--r--app/services/create_release_service.rb2
-rw-r--r--app/services/create_tag_service.rb2
-rw-r--r--app/services/delete_branch_service.rb2
-rw-r--r--app/services/delete_merged_branches_service.rb16
-rw-r--r--app/services/delete_tag_service.rb2
-rw-r--r--app/services/destroy_group_service.rb14
-rw-r--r--app/services/discussions/base_service.rb4
-rw-r--r--app/services/discussions/resolve_service.rb24
-rw-r--r--app/services/files/create_dir_service.rb2
-rw-r--r--app/services/files/create_service.rb2
-rw-r--r--app/services/files/delete_service.rb2
-rw-r--r--app/services/files/multi_service.rb2
-rw-r--r--app/services/files/update_service.rb2
-rw-r--r--app/services/git_hooks_service.rb6
-rw-r--r--app/services/git_push_service.rb44
-rw-r--r--app/services/groups/update_service.rb2
-rw-r--r--app/services/issuable_base_service.rb26
-rw-r--r--app/services/issues/base_service.rb8
-rw-r--r--app/services/issues/build_service.rb50
-rw-r--r--app/services/issues/close_service.rb2
-rw-r--r--app/services/issues/create_service.rb14
-rw-r--r--app/services/issues/update_service.rb2
-rw-r--r--app/services/labels/find_or_create_service.rb7
-rw-r--r--app/services/merge_requests/add_todo_when_build_fails_service.rb7
-rw-r--r--app/services/merge_requests/base_service.rb4
-rw-r--r--app/services/merge_requests/build_service.rb8
-rw-r--r--app/services/merge_requests/merge_when_build_succeeds_service.rb45
-rw-r--r--app/services/merge_requests/merge_when_pipeline_succeeds_service.rb45
-rw-r--r--app/services/merge_requests/refresh_service.rb14
-rw-r--r--app/services/merge_requests/update_service.rb6
-rw-r--r--app/services/notes/create_service.rb11
-rw-r--r--app/services/notes/delete_service.rb1
-rw-r--r--app/services/notes/update_service.rb1
-rw-r--r--app/services/notification_service.rb56
-rw-r--r--app/services/oauth2/access_token_validation_service.rb42
-rw-r--r--app/services/projects/create_service.rb11
-rw-r--r--app/services/projects/participants_service.rb9
-rw-r--r--app/services/projects/transfer_service.rb3
-rw-r--r--app/services/projects/update_service.rb2
-rw-r--r--app/services/slash_commands/interpret_service.rb4
-rw-r--r--app/services/system_note_service.rb98
-rw-r--r--app/services/update_release_service.rb2
-rw-r--r--app/services/user_project_access_changed_service.rb9
-rw-r--r--app/uploaders/artifact_uploader.rb10
-rw-r--r--app/uploaders/attachment_uploader.rb2
-rw-r--r--app/uploaders/avatar_uploader.rb8
-rw-r--r--app/uploaders/file_uploader.rb2
-rw-r--r--app/uploaders/gitlab_uploader.rb11
-rw-r--r--app/uploaders/lfs_object_uploader.rb10
-rw-r--r--app/uploaders/uploader_helper.rb2
-rw-r--r--app/validators/namespace_validator.rb22
-rw-r--r--app/validators/project_path_validator.rb36
-rw-r--r--app/views/admin/abuse_reports/index.html.haml10
-rw-r--r--app/views/admin/application_settings/_form.html.haml47
-rw-r--r--app/views/admin/applications/_form.html.haml6
-rw-r--r--app/views/admin/applications/show.html.haml6
-rw-r--r--app/views/admin/builds/index.html.haml2
-rw-r--r--app/views/admin/dashboard/_head.html.haml2
-rw-r--r--app/views/admin/dashboard/index.html.haml14
-rw-r--r--app/views/admin/groups/_form.html.haml2
-rw-r--r--app/views/admin/groups/_group.html.haml4
-rw-r--r--app/views/admin/groups/edit.html.haml2
-rw-r--r--app/views/admin/groups/new.html.haml2
-rw-r--r--app/views/admin/groups/show.html.haml6
-rw-r--r--app/views/admin/logs/show.html.haml2
-rw-r--r--app/views/admin/projects/index.html.haml12
-rw-r--r--app/views/admin/runners/show.html.haml2
-rw-r--r--app/views/admin/services/_form.html.haml7
-rw-r--r--app/views/admin/users/_user.html.haml8
-rw-r--r--app/views/admin/users/index.html.haml2
-rw-r--r--app/views/admin/users/projects.html.haml2
-rw-r--r--app/views/award_emoji/_awards_block.html.haml5
-rw-r--r--app/views/ci/status/_badge.html.haml10
-rw-r--r--app/views/ci/status/_graph_badge.html.haml19
-rw-r--r--app/views/dashboard/_activity_head.html.haml4
-rw-r--r--app/views/dashboard/_projects_head.html.haml6
-rw-r--r--app/views/dashboard/_snippets_head.html.haml20
-rw-r--r--app/views/dashboard/projects/_zero_authorized_projects.html.haml23
-rw-r--r--app/views/dashboard/snippets/index.html.haml42
-rw-r--r--app/views/dashboard/todos/index.html.haml16
-rw-r--r--app/views/devise/confirmations/almost_there.haml7
-rw-r--r--app/views/devise/sessions/_new_base.html.haml4
-rw-r--r--app/views/devise/sessions/new.html.haml1
-rw-r--r--app/views/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml3
-rw-r--r--app/views/discussions/_discussion.html.haml6
-rw-r--r--app/views/doorkeeper/applications/_form.html.haml4
-rw-r--r--app/views/doorkeeper/applications/show.html.haml5
-rw-r--r--app/views/errors/access_denied.html.haml16
-rw-r--r--app/views/errors/encoding.html.haml14
-rw-r--r--app/views/errors/git_not_found.html.haml17
-rw-r--r--app/views/errors/not_found.html.haml14
-rw-r--r--app/views/errors/omniauth_error.html.haml16
-rw-r--r--app/views/events/_event.html.haml21
-rw-r--r--app/views/events/event/_push.html.haml6
-rw-r--r--app/views/explore/_head.html.haml4
-rw-r--r--app/views/explore/groups/index.html.haml4
-rw-r--r--app/views/explore/projects/_filter.html.haml8
-rw-r--r--app/views/explore/snippets/index.html.haml10
-rw-r--r--app/views/groups/group_members/index.html.haml1
-rw-r--r--app/views/groups/group_members/update.js.haml1
-rw-r--r--app/views/groups/issues.html.haml37
-rw-r--r--app/views/groups/merge_requests.html.haml5
-rw-r--r--app/views/groups/milestones/new.html.haml15
-rw-r--r--app/views/groups/show.html.haml26
-rw-r--r--app/views/help/show.html.haml2
-rw-r--r--app/views/import/bitbucket/status.html.haml45
-rw-r--r--app/views/invites/show.html.haml13
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--app/views/layouts/_init_auto_complete.html.haml14
-rw-r--r--app/views/layouts/_page.html.haml9
-rw-r--r--app/views/layouts/_search.html.haml2
-rw-r--r--app/views/layouts/_user_styles.html.haml24
-rw-r--r--app/views/layouts/errors.html.haml65
-rw-r--r--app/views/layouts/header/_default.html.haml1
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml5
-rw-r--r--app/views/layouts/nav/_group.html.haml2
-rw-r--r--app/views/layouts/nav/_group_settings.html.haml12
-rw-r--r--app/views/layouts/nav/_profile.html.haml4
-rw-r--r--app/views/layouts/nav/_project.html.haml21
-rw-r--r--app/views/notify/_note_message.text.erb5
-rw-r--r--app/views/notify/_note_mr_or_commit_email.html.haml18
-rw-r--r--app/views/notify/_note_mr_or_commit_email.text.erb8
-rw-r--r--app/views/notify/_simple_diff.text.erb3
-rw-r--r--app/views/notify/links/ci/builds/_build.html.haml2
-rw-r--r--app/views/notify/links/ci/builds/_build.text.erb1
-rw-r--r--app/views/notify/links/generic_commit_statuses/_generic_commit_status.html.haml1
-rw-r--r--app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb1
-rw-r--r--app/views/notify/note_commit_email.html.haml4
-rw-r--r--app/views/notify/note_commit_email.text.erb11
-rw-r--r--app/views/notify/note_merge_request_email.html.haml9
-rw-r--r--app/views/notify/note_merge_request_email.text.erb11
-rw-r--r--app/views/notify/pipeline_failed_email.html.haml12
-rw-r--r--app/views/notify/pipeline_failed_email.text.erb4
-rw-r--r--app/views/notify/pipeline_success_email.html.haml2
-rw-r--r--app/views/notify/pipeline_success_email.text.erb2
-rw-r--r--app/views/notify/repository_push_email.html.haml13
-rw-r--r--app/views/profiles/chat_names/_chat_name.html.haml27
-rw-r--r--app/views/profiles/chat_names/index.html.haml30
-rw-r--r--app/views/profiles/chat_names/new.html.haml15
-rw-r--r--app/views/profiles/notifications/show.html.haml2
-rw-r--r--app/views/profiles/personal_access_tokens/_form.html.haml21
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml17
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml2
-rw-r--r--app/views/profiles/update_username.js.haml3
-rw-r--r--app/views/projects/_activity.html.haml2
-rw-r--r--app/views/projects/_last_commit.html.haml2
-rw-r--r--app/views/projects/_merge_request_merge_settings.html.haml15
-rw-r--r--app/views/projects/_merge_request_settings.html.haml26
-rw-r--r--app/views/projects/_readme.html.haml2
-rw-r--r--app/views/projects/artifacts/browse.html.haml1
-rw-r--r--app/views/projects/blame/show.html.haml4
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/boards/_show.html.haml28
-rw-r--r--app/views/projects/boards/components/_blank_state.html.haml2
-rw-r--r--app/views/projects/boards/components/_board.html.haml112
-rw-r--r--app/views/projects/boards/components/_board_list.html.haml44
-rw-r--r--app/views/projects/boards/components/_card.html.haml64
-rw-r--r--app/views/projects/boards/components/_sidebar.html.haml2
-rw-r--r--app/views/projects/boards/components/sidebar/_assignee.html.haml2
-rw-r--r--app/views/projects/boards/components/sidebar/_due_date.html.haml2
-rw-r--r--app/views/projects/boards/components/sidebar/_labels.html.haml2
-rw-r--r--app/views/projects/boards/components/sidebar/_milestone.html.haml2
-rw-r--r--app/views/projects/boards/components/sidebar/_notifications.html.haml12
-rw-r--r--app/views/projects/boards/index.html.haml19
-rw-r--r--app/views/projects/boards/show.html.haml19
-rw-r--r--app/views/projects/branches/index.html.haml6
-rw-r--r--app/views/projects/builds/_header.html.haml7
-rw-r--r--app/views/projects/builds/_sidebar.html.haml9
-rw-r--r--app/views/projects/builds/_table.html.haml3
-rw-r--r--app/views/projects/builds/show.html.haml50
-rw-r--r--app/views/projects/buttons/_download.html.haml79
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml31
-rw-r--r--app/views/projects/ci/builds/_build_pipeline.html.haml13
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml17
-rw-r--r--app/views/projects/commit/_change.html.haml11
-rw-r--r--app/views/projects/commit/_ci_stage.html.haml15
-rw-r--r--app/views/projects/commit/_commit_box.html.haml24
-rw-r--r--app/views/projects/commit/_pipeline.html.haml23
-rw-r--r--app/views/projects/commit/_pipeline_stage.html.haml14
-rw-r--r--app/views/projects/commit/_pipeline_status_group.html.haml13
-rw-r--r--app/views/projects/commit/_pipelines_list.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml9
-rw-r--r--app/views/projects/cycle_analytics/_empty_stage.html.haml7
-rw-r--r--app/views/projects/cycle_analytics/_no_access.html.haml7
-rw-r--r--app/views/projects/cycle_analytics/_overview.html.haml15
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml99
-rw-r--r--app/views/projects/diffs/_content.html.haml4
-rw-r--r--app/views/projects/diffs/_diffs.html.haml5
-rw-r--r--app/views/projects/diffs/_file.html.haml6
-rw-r--r--app/views/projects/diffs/_line.html.haml4
-rw-r--r--app/views/projects/diffs/_stats.html.haml11
-rw-r--r--app/views/projects/edit.html.haml18
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--app/views/projects/environments/_environment.html.haml35
-rw-r--r--app/views/projects/environments/_header_title.html.haml1
-rw-r--r--app/views/projects/environments/index.html.haml60
-rw-r--r--app/views/projects/forks/error.html.haml6
-rw-r--r--app/views/projects/forks/index.html.haml4
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml91
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml10
-rw-r--r--app/views/projects/group_links/update.js.haml1
-rw-r--r--app/views/projects/imports/show.html.haml2
-rw-r--r--app/views/projects/issues/_issues.html.haml3
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml22
-rw-r--r--app/views/projects/issues/index.html.haml24
-rw-r--r--app/views/projects/issues/show.html.haml1
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml11
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml8
-rw-r--r--app/views/projects/merge_requests/_show.html.haml29
-rw-r--r--app/views/projects/merge_requests/conflicts.html.haml5
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml5
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml10
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_commits.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_versions.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml8
-rw-r--r--app/views/projects/merge_requests/widget/_open.html.haml25
-rw-r--r--app/views/projects/merge_requests/widget/_show.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml6
-rw-r--r--app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml4
-rw-r--r--app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml6
-rw-r--r--app/views/projects/milestones/_form.html.haml7
-rw-r--r--app/views/projects/milestones/show.html.haml19
-rw-r--r--app/views/projects/new.html.haml3
-rw-r--r--app/views/projects/notes/_note.html.haml13
-rw-r--r--app/views/projects/pipelines/_graph.html.haml4
-rw-r--r--app/views/projects/pipelines/_info.html.haml72
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml42
-rw-r--r--app/views/projects/pipelines/index.html.haml11
-rw-r--r--app/views/projects/pipelines/show.html.haml10
-rw-r--r--app/views/projects/pipelines_settings/_badge.html.haml2
-rw-r--r--app/views/projects/pipelines_settings/show.html.haml3
-rw-r--r--app/views/projects/project_members/index.html.haml1
-rw-r--r--app/views/projects/project_members/update.js.haml1
-rw-r--r--app/views/projects/refs/logs_tree.js.haml5
-rw-r--r--app/views/projects/services/_form.html.haml19
-rw-r--r--app/views/projects/services/index.html.haml5
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml100
-rw-r--r--app/views/projects/show.html.haml7
-rw-r--r--app/views/projects/snippets/_actions.html.haml14
-rw-r--r--app/views/projects/snippets/index.html.haml20
-rw-r--r--app/views/projects/snippets/show.html.haml5
-rw-r--r--app/views/projects/stage/_graph.html.haml21
-rw-r--r--app/views/projects/stage/_in_stage_group.html.haml13
-rw-r--r--app/views/projects/stage/_stage.html.haml13
-rw-r--r--app/views/projects/tags/index.html.haml11
-rw-r--r--app/views/projects/tags/new.html.haml2
-rw-r--r--app/views/projects/tags/show.html.haml21
-rw-r--r--app/views/projects/tree/_tree_content.html.haml9
-rw-r--r--app/views/projects/triggers/index.html.haml20
-rw-r--r--app/views/projects/variables/_table.html.haml4
-rw-r--r--app/views/projects/variables/index.html.haml1
-rw-r--r--app/views/projects/wikis/_form.html.haml3
-rw-r--r--app/views/projects/wikis/_nav.html.haml16
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml23
-rw-r--r--app/views/projects/wikis/edit.html.haml32
-rw-r--r--app/views/projects/wikis/git_access.html.haml52
-rw-r--r--app/views/projects/wikis/history.html.haml17
-rw-r--r--app/views/projects/wikis/pages.html.haml13
-rw-r--r--app/views/projects/wikis/show.html.haml17
-rw-r--r--app/views/repository_check_mailer/notify.html.haml2
-rw-r--r--app/views/repository_check_mailer/notify.text.haml2
-rw-r--r--app/views/search/_filter.html.haml4
-rw-r--r--app/views/search/results/_blob.html.haml12
-rw-r--r--app/views/search/results/_commit.html.haml2
-rw-r--r--app/views/search/results/_snippet_title.html.haml2
-rw-r--r--app/views/shared/_commit_message_container.html.haml9
-rw-r--r--app/views/shared/_event_filter.html.haml2
-rw-r--r--app/views/shared/_group_form.html.haml5
-rw-r--r--app/views/shared/_issues.html.haml4
-rw-r--r--app/views/shared/_label.html.haml55
-rw-r--r--app/views/shared/_merge_requests.html.haml2
-rw-r--r--app/views/shared/_milestone_expired.html.haml6
-rw-r--r--app/views/shared/_milestones_filter.html.haml6
-rw-r--r--app/views/shared/_service_settings.html.haml84
-rw-r--r--app/views/shared/_sort_dropdown.html.haml4
-rw-r--r--app/views/shared/builds/_tabs.html.haml8
-rw-r--r--app/views/shared/empty_states/_issues.html.haml22
-rw-r--r--app/views/shared/empty_states/icons/_issues.svg1
-rw-r--r--app/views/shared/groups/_group.html.haml2
-rw-r--r--app/views/shared/icons/_delta.svg3
-rw-r--r--app/views/shared/icons/_icon_cycle_analytics_overview.svg81
-rw-r--r--app/views/shared/icons/_icon_lock.svg25
-rw-r--r--app/views/shared/icons/_icon_no_data.svg27
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_canceled.svg7
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_created.svg2
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_failed.svg7
-rwxr-xr-xapp/views/shared/icons/_icon_status_manual.svg1
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_pending.svg7
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_running.svg7
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_skipped.svg2
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_success.svg7
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_warning.svg7
-rw-r--r--app/views/shared/issuable/_filter.html.haml8
-rw-r--r--app/views/shared/issuable/_form.html.haml130
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_label_page_create.html.haml2
-rw-r--r--app/views/shared/issuable/_label_page_default.html.haml8
-rw-r--r--app/views/shared/issuable/_nav.html.haml20
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml21
-rw-r--r--app/views/shared/issuable/form/_branch_chooser.html.haml30
-rw-r--r--app/views/shared/issuable/form/_description.html.haml15
-rw-r--r--app/views/shared/issuable/form/_metadata.html.haml38
-rw-r--r--app/views/shared/issuable/form/_title.html.haml32
-rw-r--r--app/views/shared/members/_access_request_buttons.html.haml26
-rw-r--r--app/views/shared/members/_group.html.haml21
-rw-r--r--app/views/shared/members/_member.html.haml26
-rw-r--r--app/views/shared/members/_sort_dropdown.html.haml9
-rw-r--r--app/views/shared/milestones/_form_dates.html.haml15
-rw-r--r--app/views/shared/milestones/_summary.html.haml62
-rw-r--r--app/views/shared/milestones/_top.html.haml4
-rw-r--r--app/views/shared/notifications/_button.html.haml5
-rw-r--r--app/views/shared/notifications/_custom_notifications.html.haml2
-rw-r--r--app/views/shared/notifications/_notification_dropdown.html.haml3
-rw-r--r--app/views/shared/snippets/_header.html.haml12
-rw-r--r--app/views/shared/snippets/_snippet.html.haml31
-rw-r--r--app/views/shared/tokens/_scopes_form.html.haml9
-rw-r--r--app/views/shared/tokens/_scopes_list.html.haml13
-rw-r--r--app/views/snippets/_actions.html.haml14
-rw-r--r--app/views/snippets/_snippets.html.haml3
-rw-r--r--app/views/snippets/_snippets_scope_menu.html.haml31
-rw-r--r--app/views/snippets/show.html.haml4
-rw-r--r--app/workers/authorized_projects_worker.rb34
-rw-r--r--app/workers/build_success_worker.rb4
-rw-r--r--app/workers/delete_merged_branches_worker.rb20
-rw-r--r--app/workers/new_note_worker.rb14
-rw-r--r--app/workers/pipeline_metrics_worker.rb4
-rw-r--r--app/workers/pipeline_notification_worker.rb12
-rw-r--r--app/workers/pipeline_success_worker.rb2
-rw-r--r--app/workers/process_commit_worker.rb25
-rw-r--r--app/workers/project_cache_worker.rb54
-rwxr-xr-xbin/changelog2
-rwxr-xr-xbin/rspec-stackprof16
-rw-r--r--changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml4
-rw-r--r--changelogs/unreleased/18435-autocomplete-is-not-performant.yml4
-rw-r--r--changelogs/unreleased/18546-update-wiki-page-design.yml4
-rw-r--r--changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml4
-rw-r--r--changelogs/unreleased/20052-actions-table-vscroll.yml4
-rw-r--r--changelogs/unreleased/20492-access-token-scopes.yml4
-rw-r--r--changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml4
-rw-r--r--changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml4
-rw-r--r--changelogs/unreleased/22373-reduce-queries-in-api-helpers-find_project.yml4
-rw-r--r--changelogs/unreleased/22588-todos-filter-shows-all-users.yml4
-rw-r--r--changelogs/unreleased/22604-manual-actions.yml4
-rw-r--r--changelogs/unreleased/22719-provide-a-new-gitlab-workhorse-install-rake-task-similar-to-gitlab-shell-install.yml4
-rw-r--r--changelogs/unreleased/22781-user-generated-permalinks.yml4
-rw-r--r--changelogs/unreleased/22849-ci-build-ref-slug.yml4
-rw-r--r--changelogs/unreleased/22864-add-environment-slug.yml4
-rw-r--r--changelogs/unreleased/22864-kubernetes-service.yml4
-rw-r--r--changelogs/unreleased/22947-fix_issues_atom_feed_url.yml4
-rw-r--r--changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml4
-rw-r--r--changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml5
-rw-r--r--changelogs/unreleased/23500-enable-colorvariable.yml4
-rw-r--r--changelogs/unreleased/23532-define-common-helper-for-describe-pagination-params-in-api.yml4
-rw-r--r--changelogs/unreleased/23573-sort-functionality-for-project-member.yml4
-rw-r--r--changelogs/unreleased/23589-open-issue-for-mr.yml5
-rw-r--r--changelogs/unreleased/23718-backup-rake-task-human-readable.yml4
-rw-r--r--changelogs/unreleased/23961-can-t-share-project-with-groups.yml4
-rw-r--r--changelogs/unreleased/24056-guest-sees-some-project-details-and-gets-404.yml4
-rw-r--r--changelogs/unreleased/24059-round-robin-repository-storage.yml4
-rw-r--r--changelogs/unreleased/24102-cannot-unselect-remove-source-branch-when-editing-merge-request.yml4
-rw-r--r--changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml4
-rw-r--r--changelogs/unreleased/24150-consistent-dropdown-styles.yml4
-rw-r--r--changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml4
-rw-r--r--changelogs/unreleased/24413-show-unconfirmed-email-status.yml4
-rw-r--r--changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml4
-rw-r--r--changelogs/unreleased/24576_cant_stop_impersonating.yml4
-rw-r--r--changelogs/unreleased/24710-fix-generic-commit-status-table-row.yml4
-rw-r--r--changelogs/unreleased/24726-remove-across-gitlab.yml4
-rw-r--r--changelogs/unreleased/24733-archived-project-merge-request-count.yml4
-rw-r--r--changelogs/unreleased/24803-change-cursor-for-ca-stages.yml5
-rw-r--r--changelogs/unreleased/24807-stop-ddosing-ourselves.yml4
-rw-r--r--changelogs/unreleased/24824-dropdown-items-focus.yml4
-rw-r--r--changelogs/unreleased/24844-environments-date.yml4
-rw-r--r--changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml4
-rw-r--r--changelogs/unreleased/24921-hide-prompt-to-add-ssh-key-if-ssh-protocol-is-disabled.yml4
-rw-r--r--changelogs/unreleased/24927-custom-event-polyfill-test.yml4
-rw-r--r--changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml4
-rw-r--r--changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml5
-rw-r--r--changelogs/unreleased/24999-fix-project-avatar-alignment.yml4
-rw-r--r--changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml4
-rw-r--r--changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml4
-rw-r--r--changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml4
-rw-r--r--changelogs/unreleased/25031-do-not-raise-error-in-autocomplete.yml4
-rw-r--r--changelogs/unreleased/25098-header-margins-on-pipeline-settings.yml5
-rw-r--r--changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml4
-rw-r--r--changelogs/unreleased/25136-last-deployment-link.yml4
-rw-r--r--changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml4
-rw-r--r--changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml4
-rw-r--r--changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml5
-rw-r--r--changelogs/unreleased/25207-text-overflow-env-table.yml4
-rw-r--r--changelogs/unreleased/25221-fix-build-status-overflow-mobile.yml4
-rw-r--r--changelogs/unreleased/25251-actionview-template-error-undefined-method-text-for-nil-nilclass.yml5
-rw-r--r--changelogs/unreleased/25264-ref-commit.yml4
-rw-r--r--changelogs/unreleased/25272_fix_comments_tab_disappearing.yml4
-rw-r--r--changelogs/unreleased/25294-remove-signed-out-msg.yml4
-rw-r--r--changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml4
-rw-r--r--changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml4
-rw-r--r--changelogs/unreleased/25374-svg-as-prop.yml4
-rw-r--r--changelogs/unreleased/25482-fix-api-sudo.yml4
-rw-r--r--changelogs/unreleased/25483-broken-tabs.yml4
-rw-r--r--changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml4
-rw-r--r--changelogs/unreleased/25617-todos-filter-placeholder.yml4
-rw-r--r--changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml4
-rw-r--r--changelogs/unreleased/4269-public-api.yml4
-rw-r--r--changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml3
-rw-r--r--changelogs/unreleased/8003-katex-math.yml4
-rw-r--r--changelogs/unreleased/abuse_report-fixture.yml4
-rw-r--r--changelogs/unreleased/add-api-label-id.yml4
-rw-r--r--changelogs/unreleased/add-project-import-data-index.yml4
-rw-r--r--changelogs/unreleased/add_info_to_qr.yml4
-rw-r--r--changelogs/unreleased/allow-more-filenames.yml4
-rw-r--r--changelogs/unreleased/api-branch-status.yml4
-rw-r--r--changelogs/unreleased/api-cherry-pick.yml4
-rw-r--r--changelogs/unreleased/api-delete-group-share.yml4
-rw-r--r--changelogs/unreleased/api-expose-commiter-details.yml4
-rw-r--r--changelogs/unreleased/api-label-priorities.yml4
-rw-r--r--changelogs/unreleased/api-remove-source-branch.yml4
-rw-r--r--changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml4
-rw-r--r--changelogs/unreleased/api-simple-group-project.yml4
-rw-r--r--changelogs/unreleased/awards_handler.yml4
-rw-r--r--changelogs/unreleased/bitbucket-oauth2.yml4
-rw-r--r--changelogs/unreleased/broken-link-frontend-dev-guide.yml4
-rw-r--r--changelogs/unreleased/change_development_build_fixtures.yml4
-rw-r--r--changelogs/unreleased/chomp-git-status-message.yml5
-rw-r--r--changelogs/unreleased/cleanup-common_utils-js.yml4
-rw-r--r--changelogs/unreleased/comments-fixture.yml4
-rw-r--r--changelogs/unreleased/create-dynamic-fixture-for-build_spec.yml4
-rw-r--r--changelogs/unreleased/destroy-session.yml4
-rw-r--r--changelogs/unreleased/dev-issue-24554.yml4
-rw-r--r--changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml4
-rw-r--r--changelogs/unreleased/dz-allow-nested-group-routing.yml4
-rw-r--r--changelogs/unreleased/dz-nested-groups.yml4
-rw-r--r--changelogs/unreleased/emoji-btn-disabled.yml4
-rw-r--r--changelogs/unreleased/enable-asciidoctor-admonition-icons.yml4
-rw-r--r--changelogs/unreleased/expose-deployment-variables.yml4
-rw-r--r--changelogs/unreleased/feature-admin-user-groups-link.yml4
-rw-r--r--changelogs/unreleased/features-api-snippets.yml4
-rw-r--r--changelogs/unreleased/file-template-dropwdown-proper-position.yml4
-rw-r--r--changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml4
-rw-r--r--changelogs/unreleased/fix-cancelling-pipelines.yml4
-rw-r--r--changelogs/unreleased/fix-create-pipeline-with-builds-in-transaction.yml4
-rw-r--r--changelogs/unreleased/fix-drop-project-authorized-for-user.yml4
-rw-r--r--changelogs/unreleased/fix-invalid-filename-eslint.yml4
-rw-r--r--changelogs/unreleased/fix-milestone-summary.yml4
-rw-r--r--changelogs/unreleased/fix-rename-mwbs-to-merge-when-pipeline-succeeds.yml4
-rw-r--r--changelogs/unreleased/fix-slack-pipeline-message-by-api.yml4
-rw-r--r--changelogs/unreleased/gem-update-grape.yml4
-rw-r--r--changelogs/unreleased/git-gc-improvements.yml4
-rw-r--r--changelogs/unreleased/gitlab-workhorse-multipart.yml4
-rw-r--r--changelogs/unreleased/glm-shorthand-reference.yml4
-rw-r--r--changelogs/unreleased/group-members-in-project-members-view.yml4
-rw-r--r--changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml4
-rw-r--r--changelogs/unreleased/html-safe-diff-line-content.yml4
-rw-r--r--changelogs/unreleased/improve-invite-accept-page.yml4
-rw-r--r--changelogs/unreleased/issuable_filters_present-refactor.yml4
-rw-r--r--changelogs/unreleased/issue-24534.yml4
-rw-r--r--changelogs/unreleased/issue-boards-scrollable-element.yml4
-rw-r--r--changelogs/unreleased/issue-events-filter.yml4
-rw-r--r--changelogs/unreleased/issue_13270.yml4
-rw-r--r--changelogs/unreleased/issue_22269.yml4
-rw-r--r--changelogs/unreleased/issue_23032.yml4
-rw-r--r--changelogs/unreleased/issue_24020.yml4
-rw-r--r--changelogs/unreleased/issue_24363.yml4
-rw-r--r--changelogs/unreleased/issue_24748.yml4
-rw-r--r--changelogs/unreleased/issue_24958.yml4
-rw-r--r--changelogs/unreleased/issue_25030.yml4
-rw-r--r--changelogs/unreleased/issues-1608-text.yml4
-rw-r--r--changelogs/unreleased/jej-22869.yml4
-rw-r--r--changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml4
-rw-r--r--changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml4
-rw-r--r--changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml4
-rw-r--r--changelogs/unreleased/jej-note-search-uses-finder.yml4
-rw-r--r--changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml4
-rw-r--r--changelogs/unreleased/members-dropdowns.yml4
-rw-r--r--changelogs/unreleased/milestone_start_date.yml4
-rw-r--r--changelogs/unreleased/move-abuse-report-spinach-test-to-rspec.yml4
-rw-r--r--changelogs/unreleased/move-admin-abuse-report-spinach-test-to-rspec.yml4
-rw-r--r--changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml4
-rw-r--r--changelogs/unreleased/move-admin-hooks-spinach-test-to-rspec.yml4
-rw-r--r--changelogs/unreleased/move-admin-logs-spinach-test-to-rspec.yml4
-rw-r--r--changelogs/unreleased/move-admin-spam-spinach-test-to-rspec.yml4
-rw-r--r--changelogs/unreleased/mr-origin-7855.yml4
-rw-r--r--changelogs/unreleased/post_receive-any-email.yml4
-rw-r--r--changelogs/unreleased/process-commit-worker-migration-encoding.yml4
-rw-r--r--changelogs/unreleased/process-commits-using-sidekiq.yml4
-rw-r--r--changelogs/unreleased/public-tags-api.yml4
-rw-r--r--changelogs/unreleased/readme-link-fix.yml4
-rw-r--r--changelogs/unreleased/refactor-create-service-spec.yml4
-rw-r--r--changelogs/unreleased/remove-backup-strategies.yml4
-rw-r--r--changelogs/unreleased/remove-jsx-react-eslint-plugins.yml5
-rw-r--r--changelogs/unreleased/remove-require-from-services.yml4
-rw-r--r--changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml4
-rw-r--r--changelogs/unreleased/remove-unnecessary-self-from-user-model.yml4
-rw-r--r--changelogs/unreleased/removing_unnecessary_indexes.yml4
-rw-r--r--changelogs/unreleased/render-svg-in-diffs-and-notes.yml4
-rw-r--r--changelogs/unreleased/repository-name-emojis4
-rw-r--r--changelogs/unreleased/right-sidebar-fixture.yml4
-rw-r--r--changelogs/unreleased/rounded-labels-fixes.yml4
-rw-r--r--changelogs/unreleased/rs-project-team-helpers.yml4
-rw-r--r--changelogs/unreleased/seed-runner-token.yml5
-rw-r--r--changelogs/unreleased/sh-bump-omniauth-gitlab.yml4
-rw-r--r--changelogs/unreleased/shortcuts-issuable-fixture.yml4
-rw-r--r--changelogs/unreleased/show-commit-status-from-latest-pipeline.yml4
-rw-r--r--changelogs/unreleased/show-status-from-branch.yml4
-rw-r--r--changelogs/unreleased/sidekiq_default_retries.yml4
-rw-r--r--changelogs/unreleased/simplify-create-new-list-issue-boards.yml4
-rw-r--r--changelogs/unreleased/small-emoji-adjustments.yml4
-rw-r--r--changelogs/unreleased/timeago-perf-fix.yml4
-rw-r--r--changelogs/unreleased/unescape-relative-path.yml4
-rw-r--r--changelogs/unreleased/update-api-spec-files.yml4
-rw-r--r--changelogs/unreleased/update-button-font-weight.yml4
-rw-r--r--changelogs/unreleased/update-git-version-in-doc.yml4
-rw-r--r--changelogs/unreleased/use-separate-token-for-incoming-email.yml4
-rw-r--r--changelogs/unreleased/use-st-commits-where-possible.yml5
-rw-r--r--changelogs/unreleased/username-exists-root.yml4
-rw-r--r--changelogs/unreleased/validate-state-param-when-filtering-issuables.yml4
-rw-r--r--changelogs/unreleased/zen-mode-fixture.yml4
-rw-r--r--changelogs/unreleased/zj-expose-coverage-pipelines.yml4
-rw-r--r--changelogs/unreleased/zj-fix-label-creation-non-members.yml4
-rw-r--r--changelogs/unreleased/zj-guest-reads-public-builds.yml4
-rw-r--r--changelogs/unreleased/zj-issue-new-over-issue-create.yml4
-rw-r--r--changelogs/unreleased/zj-issue-search-slash-command.yml4
-rw-r--r--changelogs/unreleased/zj-use-ruby-2-3-3.yml4
-rw-r--r--config/README.md22
-rw-r--r--config/application.rb8
-rw-r--r--config/database.yml.mysql12
-rw-r--r--config/gitlab.yml.example8
-rw-r--r--config/initializers/1_settings.rb10
-rw-r--r--config/initializers/ar_monkey_patch.rb17
-rw-r--r--config/initializers/devise.rb23
-rw-r--r--config/initializers/doorkeeper.rb4
-rw-r--r--config/initializers/email_template_interceptor.rb2
-rw-r--r--config/initializers/math_lexer.rb2
-rw-r--r--config/initializers/omniauth.rb6
-rw-r--r--config/initializers/public_key.rb2
-rw-r--r--config/initializers/rack_attack_logging.rb7
-rw-r--r--config/initializers/sentry.rb2
-rw-r--r--config/initializers/sidekiq.rb6
-rw-r--r--config/initializers/workhorse_multipart.rb3
-rw-r--r--config/locales/doorkeeper.en.yml1
-rw-r--r--config/mail_room.yml2
-rw-r--r--config/no_todos_messages.yml11
-rw-r--r--config/routes.rb21
-rw-r--r--config/routes/admin.rb23
-rw-r--r--config/routes/git_http.rb68
-rw-r--r--config/routes/group.rb44
-rw-r--r--config/routes/profile.rb6
-rw-r--r--config/routes/project.rb83
-rw-r--r--config/routes/repository.rb122
-rw-r--r--config/routes/user.rb3
-rw-r--r--config/routes/wiki.rb27
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--config/unicorn.rb.example4
-rw-r--r--db/fixtures/development/06_teams.rb32
-rw-r--r--db/fixtures/development/14_pipelines.rb2
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb2
-rw-r--r--db/fixtures/production/010_settings.rb16
-rw-r--r--db/fixtures/test/001_repo.rb0
-rw-r--r--db/migrate/20130319214458_create_forked_project_links.rb4
-rw-r--r--db/migrate/20130506090604_create_deploy_keys_projects.rb4
-rw-r--r--db/migrate/20130617095603_create_users_groups.rb4
-rw-r--r--db/migrate/20130711063759_create_project_group_links.rb4
-rw-r--r--db/migrate/20131112114325_create_broadcast_messages.rb4
-rw-r--r--db/migrate/20140122112253_create_merge_request_diffs.rb4
-rw-r--r--db/migrate/20140209025651_create_emails.rb6
-rw-r--r--db/migrate/20140625115202_create_users_star_projects.rb4
-rw-r--r--db/migrate/20140729134820_create_labels.rb4
-rw-r--r--db/migrate/20140729140420_create_label_links.rb4
-rw-r--r--db/migrate/20140914113604_add_members_table.rb4
-rw-r--r--db/migrate/20140914173417_remove_old_member_tables.rb6
-rw-r--r--db/migrate/20141006143943_move_slack_service_to_webhook.rb6
-rw-r--r--db/migrate/20141118150935_add_audit_event.rb4
-rw-r--r--db/migrate/20141216155758_create_doorkeeper_tables.rb4
-rw-r--r--db/migrate/20150108073740_create_application_settings.rb4
-rw-r--r--db/migrate/20150313012111_create_subscriptions_table.rb8
-rw-r--r--db/migrate/20150806104937_create_abuse_reports.rb4
-rw-r--r--db/migrate/20151103134857_create_lfs_objects.rb4
-rw-r--r--db/migrate/20151103134958_create_lfs_objects_projects.rb4
-rw-r--r--db/migrate/20151105094515_create_releases.rb4
-rw-r--r--db/migrate/20160212123307_create_tasks.rb4
-rw-r--r--db/migrate/20160416180807_add_award_emoji.rb4
-rw-r--r--db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb19
-rw-r--r--db/migrate/20160831214002_create_project_features.rb2
-rw-r--r--db/migrate/20160914131004_only_allow_merge_if_all_discussions_are_resolved.rb5
-rw-r--r--db/migrate/20161010142410_create_project_authorizations.rb15
-rw-r--r--db/migrate/20161017091941_add_authorized_projects_populated_to_users.rb9
-rw-r--r--db/migrate/20161020075734_default_request_access_groups.rb12
-rw-r--r--db/migrate/20161020075830_default_request_access_projects.rb12
-rw-r--r--db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb33
-rw-r--r--db/migrate/20161031171301_add_project_id_to_subscriptions.rb14
-rw-r--r--db/migrate/20161031174110_migrate_subscriptions_project_id.rb44
-rw-r--r--db/migrate/20161031181638_add_unique_index_to_subscriptions.rb18
-rw-r--r--db/migrate/20161103191444_add_sidekiq_throttling_to_application_settings.rb31
-rw-r--r--db/migrate/20161113184239_create_user_chat_names_table.rb21
-rw-r--r--db/migrate/20161115173905_add_start_date_to_milestones.rb12
-rw-r--r--db/migrate/20161117114805_remove_undeleted_groups.rb57
-rw-r--r--db/migrate/20161118183841_add_commit_events_to_services.rb15
-rw-r--r--db/migrate/20161124111390_add_parent_id_to_namespace.rb12
-rw-r--r--db/migrate/20161124111395_add_index_to_parent_id.rb14
-rw-r--r--db/migrate/20161124111402_add_routes_table.rb18
-rw-r--r--db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb102
-rw-r--r--db/migrate/20161128142110_remove_unnecessary_indexes.rb33
-rw-r--r--db/migrate/20161128161412_add_html_emails_enabled_to_application_settings.rb29
-rw-r--r--db/migrate/20161130095245_fill_routes_table.rb21
-rw-r--r--db/migrate/20161130101252_fill_projects_routes_table.rb30
-rw-r--r--db/migrate/20161202152031_remove_duplicates_from_routes.rb29
-rw-r--r--db/migrate/20161202152035_add_index_to_routes.rb16
-rw-r--r--db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb36
-rw-r--r--db/migrate/20161206153751_add_path_index_to_namespace.rb20
-rw-r--r--db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb36
-rw-r--r--db/migrate/20161206153754_add_name_index_to_namespace.rb20
-rw-r--r--db/migrate/20161207231620_fixup_environment_name_uniqueness.rb53
-rw-r--r--db/migrate/20161207231621_create_environment_name_unique_index.rb18
-rw-r--r--db/migrate/20161207231626_add_environment_slug.rb60
-rw-r--r--db/migrate/20161209153400_add_unique_index_for_environment_slug.rb15
-rw-r--r--db/migrate/20161212142807_add_lower_path_index_to_routes.rb22
-rw-r--r--db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb14
-rw-r--r--db/post_migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb19
-rw-r--r--db/post_migrate/20161011222551_remove_inactive_jira_service_properties.rb (renamed from db/migrate/20161011222551_remove_inactive_jira_service_properties.rb)0
-rw-r--r--db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb49
-rw-r--r--db/schema.rb78
-rw-r--r--doc/README.md5
-rw-r--r--doc/administration/auth/README.md3
-rw-r--r--doc/administration/auth/img/okta_admin_panel.pngbin0 -> 26164 bytes
-rw-r--r--doc/administration/auth/img/okta_saml_settings.pngbin0 -> 25470 bytes
-rw-r--r--doc/administration/auth/ldap.md24
-rw-r--r--doc/administration/auth/okta.md160
-rw-r--r--doc/administration/build_artifacts.md2
-rw-r--r--doc/administration/custom_hooks.md28
-rw-r--r--doc/administration/environment_variables.md23
-rw-r--r--doc/administration/high_availability/README.md33
-rw-r--r--doc/administration/high_availability/database.md10
-rw-r--r--doc/administration/high_availability/nfs.md2
-rw-r--r--doc/administration/high_availability/redis.md902
-rw-r--r--doc/administration/high_availability/redis_source.md366
-rw-r--r--doc/administration/img/custom_hooks_error_msg.pngbin159486 -> 44922 bytes
-rw-r--r--doc/administration/img/high_availability/active-active-diagram.pngbin29607 -> 14649 bytes
-rw-r--r--doc/administration/img/high_availability/active-passive-diagram.pngbin24246 -> 11699 bytes
-rw-r--r--doc/administration/img/housekeeping_settings.pngbin27420 -> 12025 bytes
-rw-r--r--doc/administration/img/raketasks/check_repos_output.pngbin0 -> 19153 bytes
-rw-r--r--doc/administration/img/repository_storages_admin_ui.pngbin54043 -> 17760 bytes
-rw-r--r--doc/administration/logs.md36
-rw-r--r--doc/administration/monitoring/performance/img/grafana_dashboard_dropdown.pngbin14368 -> 7761 bytes
-rw-r--r--doc/administration/monitoring/performance/img/grafana_dashboard_import.pngbin18267 -> 11836 bytes
-rw-r--r--doc/administration/monitoring/performance/img/grafana_data_source_configuration.pngbin26060 -> 14700 bytes
-rw-r--r--doc/administration/monitoring/performance/img/grafana_data_source_empty.pngbin21821 -> 11963 bytes
-rw-r--r--doc/administration/monitoring/performance/img/grafana_save_icon.pngbin9107 -> 4619 bytes
-rw-r--r--doc/administration/monitoring/performance/img/metrics_gitlab_configuration_settings.pngbin61357 -> 26169 bytes
-rw-r--r--doc/administration/monitoring/performance/img/request_profile_result.pngbin9720 -> 3236 bytes
-rw-r--r--doc/administration/monitoring/performance/img/request_profiling_token.pngbin30076 -> 10229 bytes
-rw-r--r--doc/administration/operations.md1
-rw-r--r--doc/administration/operations/img/sidekiq_job_throttling.pngbin0 -> 32229 bytes
-rw-r--r--doc/administration/operations/sidekiq_job_throttling.md33
-rw-r--r--doc/administration/raketasks/check.md97
-rw-r--r--doc/administration/reply_by_email.md10
-rw-r--r--doc/api/access_requests.md8
-rw-r--r--doc/api/award_emoji.md4
-rw-r--r--doc/api/boards.md10
-rw-r--r--doc/api/branches.md28
-rw-r--r--doc/api/broadcast_messages.md4
-rw-r--r--doc/api/build_triggers.md8
-rw-r--r--doc/api/build_variables.md10
-rw-r--r--doc/api/builds.md10
-rw-r--r--doc/api/commits.md47
-rw-r--r--doc/api/enviroments.md8
-rw-r--r--doc/api/groups.md35
-rw-r--r--doc/api/issues.md32
-rw-r--r--doc/api/labels.md25
-rw-r--r--doc/api/members.md10
-rw-r--r--doc/api/merge_requests.md95
-rw-r--r--doc/api/milestones.md6
-rw-r--r--doc/api/notes.md11
-rw-r--r--doc/api/notification_settings.md12
-rw-r--r--doc/api/pipelines.md62
-rw-r--r--doc/api/projects.md92
-rw-r--r--doc/api/repositories.md54
-rw-r--r--doc/api/repository_files.md4
-rw-r--r--doc/api/services.md166
-rw-r--r--doc/api/snippets.md232
-rw-r--r--doc/api/system_hooks.md3
-rw-r--r--doc/api/tags.md26
-rw-r--r--doc/api/users.md117
-rw-r--r--doc/ci/README.md16
-rw-r--r--doc/ci/docker/using_docker_build.md101
-rw-r--r--doc/ci/environments.md538
-rw-r--r--doc/ci/examples/php.md2
-rw-r--r--doc/ci/examples/test-scala-application.md41
-rw-r--r--doc/ci/git_submodules.md86
-rw-r--r--doc/ci/img/builds_tab.pngbin3047 -> 1956 bytes
-rw-r--r--doc/ci/img/deployments_view.pngbin0 -> 19923 bytes
-rw-r--r--doc/ci/img/environments_available_staging.pngbin0 -> 10098 bytes
-rw-r--r--doc/ci/img/environments_dynamic_groups.pngbin0 -> 45349 bytes
-rw-r--r--doc/ci/img/environments_link_url.pngbin0 -> 12277 bytes
-rw-r--r--doc/ci/img/environments_link_url_deployments.pngbin0 -> 7490 bytes
-rw-r--r--doc/ci/img/environments_link_url_mr.pngbin0 -> 17947 bytes
-rw-r--r--doc/ci/img/environments_manual_action_builds.pngbin0 -> 11137 bytes
-rw-r--r--doc/ci/img/environments_manual_action_deployments.pngbin0 -> 12563 bytes
-rw-r--r--doc/ci/img/environments_manual_action_environments.pngbin0 -> 14914 bytes
-rw-r--r--doc/ci/img/environments_manual_action_pipelines.pngbin0 -> 16243 bytes
-rw-r--r--doc/ci/img/environments_manual_action_single_pipeline.pngbin0 -> 16576 bytes
-rw-r--r--doc/ci/img/environments_mr_review_app.pngbin0 -> 15366 bytes
-rw-r--r--doc/ci/img/environments_view.pngbin0 -> 21155 bytes
-rw-r--r--doc/ci/img/features_settings.pngbin15809 -> 9243 bytes
-rw-r--r--doc/ci/img/pipelines.pngbin0 -> 7516 bytes
-rw-r--r--doc/ci/pipelines.md2
-rw-r--r--doc/ci/quick_start/img/build_log.pngbin52482 -> 24461 bytes
-rw-r--r--doc/ci/quick_start/img/builds_status.pngbin41838 -> 24278 bytes
-rw-r--r--doc/ci/quick_start/img/new_commit.pngbin7587 -> 4772 bytes
-rw-r--r--doc/ci/quick_start/img/pipelines_status.pngbin89387 -> 25494 bytes
-rw-r--r--doc/ci/quick_start/img/runners_activated.pngbin22822 -> 12337 bytes
-rw-r--r--doc/ci/quick_start/img/single_commit_status_pending.pngbin29981 -> 15785 bytes
-rw-r--r--doc/ci/quick_start/img/status_pending.pngbin16205 -> 9521 bytes
-rw-r--r--doc/ci/review_apps/img/review_apps_preview_in_mr.pngbin0 -> 11723 bytes
-rw-r--r--doc/ci/review_apps/index.md124
-rw-r--r--doc/ci/triggers/README.md34
-rw-r--r--doc/ci/triggers/img/builds_page.pngbin76181 -> 29044 bytes
-rw-r--r--doc/ci/triggers/img/trigger_single_build.pngbin21152 -> 8233 bytes
-rw-r--r--doc/ci/triggers/img/trigger_variables.pngbin9315 -> 3652 bytes
-rw-r--r--doc/ci/triggers/img/triggers_page.pngbin12002 -> 5119 bytes
-rw-r--r--doc/ci/variables/README.md276
-rw-r--r--doc/ci/yaml/README.md84
-rw-r--r--doc/customization/branded_login_page/appearance.pngbin156228 -> 85263 bytes
-rw-r--r--doc/customization/branded_login_page/custom_sign_in.pngbin166674 -> 79288 bytes
-rw-r--r--doc/customization/branded_login_page/default_login_page.pngbin150538 -> 73004 bytes
-rw-r--r--doc/development/README.md4
-rw-r--r--doc/development/code_review.md30
-rw-r--r--doc/development/doc_styleguide.md71
-rw-r--r--doc/development/frontend.md51
-rw-r--r--doc/development/gitlab_architecture_diagram.pngbin23831 -> 20339 bytes
-rw-r--r--doc/development/gotchas.md127
-rw-r--r--doc/development/img/state-model-issue.pngbin0 -> 7713 bytes
-rw-r--r--doc/development/img/state-model-legend.pngbin0 -> 8496 bytes
-rw-r--r--doc/development/img/state-model-merge-request.pngbin0 -> 12459 bytes
-rw-r--r--doc/development/instrumentation.md10
-rw-r--r--doc/development/limit_ee_conflicts.md325
-rw-r--r--doc/development/migration_style_guide.md12
-rw-r--r--doc/development/object_state_models.md25
-rw-r--r--doc/development/performance.md110
-rw-r--r--doc/development/shell_commands.md8
-rw-r--r--doc/development/testing.md4
-rw-r--r--doc/development/ux_guide/animation.md42
-rw-r--r--doc/development/ux_guide/basics.md77
-rw-r--r--doc/development/ux_guide/components.md267
-rw-r--r--doc/development/ux_guide/copy.md103
-rw-r--r--doc/development/ux_guide/features.md57
-rw-r--r--doc/development/ux_guide/img/animation-dropdown.gifbin0 -> 22483 bytes
-rw-r--r--doc/development/ux_guide/img/animation-hover.gifbin0 -> 247388 bytes
-rw-r--r--doc/development/ux_guide/img/animation-quickupdate.gifbin0 -> 6441 bytes
-rw-r--r--doc/development/ux_guide/img/button-close--active.pngbin0 -> 1385 bytes
-rw-r--r--doc/development/ux_guide/img/button-close--hover.pngbin0 -> 1015 bytes
-rw-r--r--doc/development/ux_guide/img/button-close--resting.pngbin0 -> 1271 bytes
-rw-r--r--doc/development/ux_guide/img/button-danger--active.pngbin0 -> 1450 bytes
-rw-r--r--doc/development/ux_guide/img/button-danger--hover.pngbin0 -> 1095 bytes
-rw-r--r--doc/development/ux_guide/img/button-danger--resting.pngbin0 -> 1376 bytes
-rw-r--r--doc/development/ux_guide/img/button-info--active.pngbin0 -> 1442 bytes
-rw-r--r--doc/development/ux_guide/img/button-info--hover.pngbin0 -> 1079 bytes
-rw-r--r--doc/development/ux_guide/img/button-info--resting.pngbin0 -> 1296 bytes
-rw-r--r--doc/development/ux_guide/img/button-primary.pngbin0 -> 1550 bytes
-rw-r--r--doc/development/ux_guide/img/button-secondary.pngbin0 -> 2683 bytes
-rw-r--r--doc/development/ux_guide/img/button-spam--active.pngbin0 -> 1435 bytes
-rw-r--r--doc/development/ux_guide/img/button-spam--hover.pngbin0 -> 1108 bytes
-rw-r--r--doc/development/ux_guide/img/button-spam--resting.pngbin0 -> 1377 bytes
-rw-r--r--doc/development/ux_guide/img/button-success--active.pngbin0 -> 1510 bytes
-rw-r--r--doc/development/ux_guide/img/button-success--hover.pngbin0 -> 1151 bytes
-rw-r--r--doc/development/ux_guide/img/button-success--resting.pngbin0 -> 1447 bytes
-rw-r--r--doc/development/ux_guide/img/button-success-secondary--active.pngbin0 -> 1466 bytes
-rw-r--r--doc/development/ux_guide/img/button-success-secondary--hover.pngbin0 -> 1091 bytes
-rw-r--r--doc/development/ux_guide/img/button-success-secondary--resting.pngbin0 -> 1394 bytes
-rw-r--r--doc/development/ux_guide/img/button-warning--active.pngbin0 -> 1388 bytes
-rw-r--r--doc/development/ux_guide/img/button-warning--hover.pngbin0 -> 1040 bytes
-rw-r--r--doc/development/ux_guide/img/button-warning--resting.pngbin0 -> 1296 bytes
-rw-r--r--doc/development/ux_guide/img/color-blue.pngbin0 -> 3555 bytes
-rw-r--r--doc/development/ux_guide/img/color-green.pngbin0 -> 3852 bytes
-rw-r--r--doc/development/ux_guide/img/color-grey.pngbin0 -> 3523 bytes
-rw-r--r--doc/development/ux_guide/img/color-orange.pngbin0 -> 4480 bytes
-rw-r--r--doc/development/ux_guide/img/color-red.pngbin0 -> 3550 bytes
-rw-r--r--doc/development/ux_guide/img/components-alerts.pngbin0 -> 27342 bytes
-rw-r--r--doc/development/ux_guide/img/components-anchorlinks.pngbin0 -> 30089 bytes
-rw-r--r--doc/development/ux_guide/img/components-contentblock.pngbin0 -> 14190 bytes
-rw-r--r--doc/development/ux_guide/img/components-counts.pngbin0 -> 2438 bytes
-rw-r--r--doc/development/ux_guide/img/components-coverblock.pngbin0 -> 10141 bytes
-rw-r--r--doc/development/ux_guide/img/components-dateexact.pngbin0 -> 4161 bytes
-rw-r--r--doc/development/ux_guide/img/components-daterelative.pngbin0 -> 4189 bytes
-rw-r--r--doc/development/ux_guide/img/components-dropdown.pngbin0 -> 31760 bytes
-rw-r--r--doc/development/ux_guide/img/components-fileholder.pngbin0 -> 3938 bytes
-rw-r--r--doc/development/ux_guide/img/components-horizontalform.pngbin0 -> 4327 bytes
-rw-r--r--doc/development/ux_guide/img/components-listinsidepanel.pngbin0 -> 3449 bytes
-rw-r--r--doc/development/ux_guide/img/components-listwithavatar.pngbin0 -> 5749 bytes
-rw-r--r--doc/development/ux_guide/img/components-listwithhover.pngbin0 -> 2860 bytes
-rw-r--r--doc/development/ux_guide/img/components-panels.pngbin0 -> 21822 bytes
-rw-r--r--doc/development/ux_guide/img/components-referencehover.pngbin0 -> 6948 bytes
-rw-r--r--doc/development/ux_guide/img/components-referenceissues.pngbin0 -> 10009 bytes
-rw-r--r--doc/development/ux_guide/img/components-referencelabels.pngbin0 -> 4108 bytes
-rw-r--r--doc/development/ux_guide/img/components-referencemilestone.pngbin0 -> 2417 bytes
-rw-r--r--doc/development/ux_guide/img/components-referencemrs.pngbin0 -> 8859 bytes
-rw-r--r--doc/development/ux_guide/img/components-referencepeople.pngbin0 -> 5607 bytes
-rw-r--r--doc/development/ux_guide/img/components-rowcontentblock.pngbin0 -> 14315 bytes
-rw-r--r--doc/development/ux_guide/img/components-simplelist.pngbin0 -> 2781 bytes
-rw-r--r--doc/development/ux_guide/img/components-table.pngbin0 -> 6081 bytes
-rw-r--r--doc/development/ux_guide/img/components-verticalform.pngbin0 -> 4964 bytes
-rw-r--r--doc/development/ux_guide/img/copy-form-addissuebutton.pngbin0 -> 16085 bytes
-rw-r--r--doc/development/ux_guide/img/copy-form-addissueform.pngbin0 -> 25978 bytes
-rw-r--r--doc/development/ux_guide/img/copy-form-editissuebutton.pngbin0 -> 11801 bytes
-rw-r--r--doc/development/ux_guide/img/copy-form-editissueform.pngbin0 -> 25621 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-default.pngbin0 -> 567 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-ibeam.pngbin0 -> 383 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-move.pngbin0 -> 276 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-panclosed.pngbin0 -> 483 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-panopened.pngbin0 -> 622 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-pointer.pngbin0 -> 574 bytes
-rw-r--r--doc/development/ux_guide/img/features-contextualnav.pngbin0 -> 5912 bytes
-rw-r--r--doc/development/ux_guide/img/features-emptystates.pngbin0 -> 61664 bytes
-rw-r--r--doc/development/ux_guide/img/features-filters.pngbin0 -> 3924 bytes
-rw-r--r--doc/development/ux_guide/img/features-globalnav.pngbin0 -> 5780 bytes
-rw-r--r--doc/development/ux_guide/img/icon-add.pngbin0 -> 317 bytes
-rw-r--r--doc/development/ux_guide/img/icon-close.pngbin0 -> 501 bytes
-rw-r--r--doc/development/ux_guide/img/icon-edit.pngbin0 -> 546 bytes
-rw-r--r--doc/development/ux_guide/img/icon-notification.pngbin0 -> 543 bytes
-rw-r--r--doc/development/ux_guide/img/icon-rss.pngbin0 -> 834 bytes
-rw-r--r--doc/development/ux_guide/img/icon-subscribe.pngbin0 -> 760 bytes
-rw-r--r--doc/development/ux_guide/img/icon-trash.pngbin0 -> 398 bytes
-rw-r--r--doc/development/ux_guide/img/monospacefont-sample.pngbin0 -> 14282 bytes
-rw-r--r--doc/development/ux_guide/img/sourcesanspro-sample.pngbin0 -> 10948 bytes
-rw-r--r--doc/development/ux_guide/img/surfaces-contentitemtitle.pngbin0 -> 5142 bytes
-rw-r--r--doc/development/ux_guide/img/surfaces-header.pngbin0 -> 4095 bytes
-rw-r--r--doc/development/ux_guide/img/surfaces-systeminformationblock.pngbin0 -> 10423 bytes
-rw-r--r--doc/development/ux_guide/img/surfaces-ux.pngbin0 -> 4029 bytes
-rw-r--r--doc/development/ux_guide/img/tooltip-placement.pngbin0 -> 2071 bytes
-rw-r--r--doc/development/ux_guide/img/tooltip-usage.pngbin0 -> 5994 bytes
-rw-r--r--doc/development/ux_guide/index.md63
-rw-r--r--doc/development/ux_guide/principles.md17
-rw-r--r--doc/development/ux_guide/resources.md13
-rw-r--r--doc/development/ux_guide/surfaces.md47
-rw-r--r--doc/development/ux_guide/tips.md44
-rw-r--r--doc/development/ux_guide/users.md16
-rw-r--r--doc/gitlab-basics/create-project.md4
-rw-r--r--doc/gitlab-basics/img/create_new_group_info.pngbin53103 -> 20321 bytes
-rw-r--r--doc/gitlab-basics/img/create_new_group_sidebar.pngbin5396 -> 2682 bytes
-rw-r--r--doc/gitlab-basics/img/create_new_project_button.pngbin10050 -> 4196 bytes
-rw-r--r--doc/gitlab-basics/img/create_new_project_from_group.pngbin6545 -> 3194 bytes
-rw-r--r--doc/gitlab-basics/img/create_new_project_info.pngbin49451 -> 20385 bytes
-rw-r--r--doc/gitlab-basics/img/fork_choose_namespace.pngbin39253 -> 13674 bytes
-rw-r--r--doc/gitlab-basics/img/fork_new.pngbin25540 -> 10722 bytes
-rw-r--r--doc/gitlab-basics/img/merge_request_new.pngbin3596 -> 2234 bytes
-rw-r--r--doc/gitlab-basics/img/merge_request_page.pngbin91432 -> 33801 bytes
-rw-r--r--doc/gitlab-basics/img/merge_request_select_branch.pngbin50707 -> 20332 bytes
-rw-r--r--doc/gitlab-basics/img/new_issue_button.pngbin3070 -> 2010 bytes
-rw-r--r--doc/gitlab-basics/img/new_issue_page.pngbin53268 -> 21386 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings.pngbin5975 -> 3045 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys.pngbin42977 -> 16531 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.pngbin37486 -> 13447 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.pngbin18498 -> 8133 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys_title.pngbin2362 -> 1872 bytes
-rw-r--r--doc/gitlab-basics/img/project_clone_url.pngbin40490 -> 14978 bytes
-rw-r--r--doc/gitlab-basics/img/project_navbar.pngbin5745 -> 3259 bytes
-rw-r--r--doc/gitlab-basics/img/select_group_dropdown.pngbin8038 -> 3489 bytes
-rw-r--r--doc/install/database_mysql.md9
-rw-r--r--doc/install/installation.md40
-rw-r--r--doc/install/requirements.md9
-rw-r--r--doc/integration/README.md10
-rw-r--r--doc/integration/bitbucket.md115
-rw-r--r--doc/integration/github.md17
-rw-r--r--doc/integration/img/akismet_settings.pngbin26625 -> 16923 bytes
-rw-r--r--doc/integration/img/bitbucket_oauth_keys.pngbin12073 -> 5149 bytes
-rw-r--r--doc/integration/img/bitbucket_oauth_settings_page.pngbin82818 -> 5607 bytes
-rw-r--r--doc/integration/img/enabled-oauth-sign-in-sources.pngbin21767 -> 13304 bytes
-rw-r--r--doc/integration/img/facebook_api_keys.pngbin85832 -> 42308 bytes
-rw-r--r--doc/integration/img/facebook_app_settings.pngbin68086 -> 35876 bytes
-rw-r--r--doc/integration/img/facebook_website_url.pngbin19823 -> 9620 bytes
-rw-r--r--doc/integration/img/github_app.pngbin55591 -> 29330 bytes
-rw-r--r--doc/integration/img/gitlab_app.pngbin30963 -> 15402 bytes
-rw-r--r--doc/integration/img/gmail_action_buttons_for_gitlab.pngbin16020 -> 11573 bytes
-rw-r--r--doc/integration/img/google_app.pngbin29154 -> 19168 bytes
-rw-r--r--doc/integration/img/jira_add_user_to_group.pngbin41994 -> 0 bytes
-rw-r--r--doc/integration/img/jira_create_new_group.pngbin32934 -> 0 bytes
-rw-r--r--doc/integration/img/jira_create_new_group_name.pngbin9054 -> 0 bytes
-rw-r--r--doc/integration/img/jira_create_new_user.pngbin21081 -> 0 bytes
-rw-r--r--doc/integration/img/jira_group_access.pngbin32210 -> 0 bytes
-rw-r--r--doc/integration/img/jira_issue_reference.pngbin36188 -> 0 bytes
-rw-r--r--doc/integration/img/jira_merge_request_close.pngbin52556 -> 0 bytes
-rw-r--r--doc/integration/img/jira_project_name.pngbin41572 -> 0 bytes
-rw-r--r--doc/integration/img/jira_service.pngbin56834 -> 0 bytes
-rw-r--r--doc/integration/img/jira_service_close_issue.pngbin79569 -> 0 bytes
-rw-r--r--doc/integration/img/jira_service_page.pngbin45089 -> 0 bytes
-rw-r--r--doc/integration/img/jira_user_management_link.pngbin43095 -> 0 bytes
-rw-r--r--doc/integration/img/jira_workflow_screenshot.pngbin111093 -> 0 bytes
-rw-r--r--doc/integration/img/oauth_provider_admin_application.pngbin33440 -> 17082 bytes
-rw-r--r--doc/integration/img/oauth_provider_application_form.pngbin23048 -> 12566 bytes
-rw-r--r--doc/integration/img/oauth_provider_application_id_secret.pngbin27673 -> 15293 bytes
-rw-r--r--doc/integration/img/oauth_provider_authorized_application.pngbin26622 -> 14668 bytes
-rw-r--r--doc/integration/img/oauth_provider_user_wide_applications.pngbin33337 -> 17526 bytes
-rw-r--r--doc/integration/img/spam_log.pngbin187190 -> 50996 bytes
-rw-r--r--doc/integration/img/submit_issue.pngbin174556 -> 45962 bytes
-rw-r--r--doc/integration/img/twitter_app_api_keys.pngbin36921 -> 24577 bytes
-rw-r--r--doc/integration/img/twitter_app_details.pngbin64686 -> 40392 bytes
-rw-r--r--doc/integration/jira.md196
-rw-r--r--doc/integration/shibboleth.md4
-rw-r--r--doc/intro/README.md2
-rw-r--r--doc/monitoring/performance/img/grafana_dashboard_dropdown.pngbin14368 -> 7761 bytes
-rw-r--r--doc/monitoring/performance/img/grafana_dashboard_import.pngbin18267 -> 11836 bytes
-rw-r--r--doc/monitoring/performance/img/grafana_data_source_configuration.pngbin26060 -> 14700 bytes
-rw-r--r--doc/monitoring/performance/img/grafana_data_source_empty.pngbin21821 -> 11963 bytes
-rw-r--r--doc/monitoring/performance/img/grafana_save_icon.pngbin9107 -> 4619 bytes
-rw-r--r--doc/monitoring/performance/img/metrics_gitlab_configuration_settings.pngbin37228 -> 21387 bytes
-rw-r--r--doc/profile/2fa_u2f_authenticate.pngbin54413 -> 17585 bytes
-rw-r--r--doc/profile/2fa_u2f_register.pngbin112414 -> 35186 bytes
-rw-r--r--doc/project_services/img/builds_emails_service.pngbin30956 -> 19203 bytes
-rw-r--r--doc/project_services/img/emails_on_push_service.pngbin98160 -> 28535 bytes
-rw-r--r--doc/project_services/img/jira_add_user_to_group.pngbin0 -> 24838 bytes
-rw-r--r--doc/project_services/img/jira_create_new_group.pngbin0 -> 19127 bytes
-rw-r--r--doc/project_services/img/jira_create_new_group_name.pngbin0 -> 5168 bytes
-rw-r--r--doc/project_services/img/jira_create_new_user.pngbin0 -> 12625 bytes
-rw-r--r--doc/project_services/img/jira_group_access.pngbin0 -> 19235 bytes
-rw-r--r--doc/project_services/img/jira_issue_reference.pngbin0 -> 18399 bytes
-rw-r--r--doc/project_services/img/jira_merge_request_close.pngbin0 -> 21172 bytes
-rw-r--r--doc/project_services/img/jira_project_name.pngbin0 -> 26685 bytes
-rw-r--r--doc/project_services/img/jira_service.pngbin0 -> 37869 bytes
-rw-r--r--doc/project_services/img/jira_service_close_comment.pngbin0 -> 11893 bytes
-rw-r--r--doc/project_services/img/jira_service_close_issue.pngbin0 -> 30570 bytes
-rw-r--r--doc/project_services/img/jira_service_page.pngbin0 -> 12228 bytes
-rw-r--r--doc/project_services/img/jira_user_management_link.pngbin0 -> 23921 bytes
-rw-r--r--doc/project_services/img/jira_workflow_screenshot.pngbin0 -> 66685 bytes
-rw-r--r--doc/project_services/img/kubernetes_configuration.pngbin0 -> 113827 bytes
-rw-r--r--doc/project_services/img/mattermost_add_slash_command.pngbin0 -> 9265 bytes
-rw-r--r--doc/project_services/img/mattermost_bot_auth.pngbin0 -> 8676 bytes
-rw-r--r--doc/project_services/img/mattermost_bot_available_commands.pngbin0 -> 4647 bytes
-rw-r--r--doc/project_services/img/mattermost_config_help.pngbin0 -> 63138 bytes
-rw-r--r--doc/project_services/img/mattermost_configuration.pngbin0 -> 73502 bytes
-rw-r--r--doc/project_services/img/mattermost_console_integrations.pngbin0 -> 314642 bytes
-rw-r--r--doc/project_services/img/mattermost_gitlab_token.pngbin0 -> 3688 bytes
-rw-r--r--doc/project_services/img/mattermost_goto_console.pngbin0 -> 7754 bytes
-rw-r--r--doc/project_services/img/mattermost_slash_command_configuration.pngbin0 -> 24169 bytes
-rw-r--r--doc/project_services/img/mattermost_slash_command_token.pngbin0 -> 8624 bytes
-rw-r--r--doc/project_services/img/mattermost_team_integrations.pngbin0 -> 4766 bytes
-rw-r--r--doc/project_services/img/redmine_configuration.pngbin16973 -> 10266 bytes
-rw-r--r--doc/project_services/img/services_templates_redmine_example.pngbin13936 -> 8776 bytes
-rw-r--r--doc/project_services/img/slack_configuration.pngbin75762 -> 29825 bytes
-rw-r--r--doc/project_services/jira.md209
-rw-r--r--doc/project_services/kubernetes.md49
-rw-r--r--doc/project_services/mattermost.md45
-rw-r--r--doc/project_services/mattermost_slash_commands.md157
-rw-r--r--doc/project_services/project_services.md7
-rw-r--r--doc/project_services/slack.md4
-rw-r--r--doc/raketasks/backup_hrz.pngbin31784 -> 11444 bytes
-rw-r--r--doc/raketasks/backup_restore.md6
-rw-r--r--doc/raketasks/check.md62
-rw-r--r--doc/raketasks/check_repos_output.pngbin35333 -> 0 bytes
-rw-r--r--doc/security/img/two_factor_authentication_settings.pngbin16807 -> 9941 bytes
-rw-r--r--doc/ssh/README.md197
-rw-r--r--doc/university/README.md23
-rw-r--r--doc/university/high-availability/aws/img/auto-scaling-det.pngbin106157 -> 29970 bytes
-rw-r--r--doc/university/high-availability/aws/img/db-subnet-group.pngbin98632 -> 29306 bytes
-rw-r--r--doc/university/high-availability/aws/img/ec-subnet.pngbin91922 -> 28405 bytes
-rw-r--r--doc/university/high-availability/aws/img/elastic-file-system.pngbin109719 -> 34582 bytes
-rw-r--r--doc/university/high-availability/aws/img/ig-rt.pngbin42022 -> 12547 bytes
-rw-r--r--doc/university/high-availability/aws/img/ig.pngbin26220 -> 8149 bytes
-rw-r--r--doc/university/high-availability/aws/img/instance_specs.pngbin40938 -> 11525 bytes
-rw-r--r--doc/university/high-availability/aws/img/new_vpc.pngbin54072 -> 15696 bytes
-rw-r--r--doc/university/high-availability/aws/img/policies.pngbin132366 -> 39845 bytes
-rw-r--r--doc/university/high-availability/aws/img/rds-net-opt.pngbin54996 -> 16347 bytes
-rw-r--r--doc/university/high-availability/aws/img/rds-sec-group.pngbin43950 -> 11584 bytes
-rw-r--r--doc/university/high-availability/aws/img/redis-cluster-det.pngbin81524 -> 23761 bytes
-rw-r--r--doc/university/high-availability/aws/img/redis-net.pngbin100700 -> 27261 bytes
-rw-r--r--doc/university/high-availability/aws/img/route_table.pngbin39611 -> 12088 bytes
-rw-r--r--doc/university/high-availability/aws/img/subnet.pngbin56466 -> 17077 bytes
-rwxr-xr-xdoc/university/training/gitlab_flow.md53
-rw-r--r--doc/university/training/gitlab_flow/feature_branches.pngbin0 -> 6202 bytes
-rw-r--r--doc/university/training/gitlab_flow/production_branch.pngbin0 -> 7293 bytes
-rw-r--r--doc/university/training/gitlab_flow/release_branches.pngbin0 -> 12775 bytes
-rwxr-xr-xdoc/university/training/index.md6
-rw-r--r--doc/university/training/logo.pngbin0 -> 8940 bytes
-rwxr-xr-xdoc/university/training/topics/additional_resources.md8
-rwxr-xr-xdoc/university/training/topics/agile_git.md33
-rwxr-xr-xdoc/university/training/topics/bisect.md81
-rwxr-xr-xdoc/university/training/topics/cherry_picking.md39
-rwxr-xr-xdoc/university/training/topics/env_setup.md60
-rwxr-xr-xdoc/university/training/topics/explore_gitlab.md10
-rwxr-xr-xdoc/university/training/topics/feature_branching.md32
-rwxr-xr-xdoc/university/training/topics/getting_started.md95
-rwxr-xr-xdoc/university/training/topics/git_add.md33
-rwxr-xr-xdoc/university/training/topics/git_intro.md24
-rwxr-xr-xdoc/university/training/topics/git_log.md57
-rwxr-xr-xdoc/university/training/topics/gitlab_flow.md53
-rwxr-xr-xdoc/university/training/topics/merge_conflicts.md70
-rwxr-xr-xdoc/university/training/topics/merge_requests.md43
-rwxr-xr-xdoc/university/training/topics/rollback_commits.md81
-rwxr-xr-xdoc/university/training/topics/stash.md86
-rwxr-xr-xdoc/university/training/topics/subtree.md55
-rwxr-xr-xdoc/university/training/topics/tags.md38
-rwxr-xr-xdoc/university/training/topics/unstage.md31
-rwxr-xr-xdoc/university/training/user_training.md392
-rw-r--r--doc/update/8.12-to-8.13.md2
-rw-r--r--doc/update/8.13-to-8.14.md4
-rw-r--r--doc/update/8.14-to-8.15.md202
-rw-r--r--doc/update/patch_versions.md14
-rw-r--r--doc/user/admin_area/img/admin_labels.pngbin91459 -> 23063 bytes
-rw-r--r--doc/user/admin_area/monitoring/img/health_check_token.pngbin6630 -> 4923 bytes
-rw-r--r--doc/user/admin_area/settings/img/access_restrictions.pngbin7435 -> 3794 bytes
-rw-r--r--doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.pngbin6227 -> 3447 bytes
-rw-r--r--doc/user/admin_area/settings/img/admin_area_settings_button.pngbin9184 -> 4403 bytes
-rw-r--r--doc/user/admin_area/settings/img/domain_blacklist.pngbin34684 -> 13606 bytes
-rw-r--r--doc/user/admin_area/settings/img/restricted_url.pngbin47539 -> 18202 bytes
-rw-r--r--doc/user/img/markdown_logo.pngbin9509 -> 4421 bytes
-rw-r--r--doc/user/markdown.md205
-rw-r--r--doc/user/permissions.md9
-rw-r--r--doc/user/project/builds/img/build_artifacts_browser.pngbin8365 -> 3782 bytes
-rw-r--r--doc/user/project/builds/img/build_artifacts_browser_button.pngbin11041 -> 4891 bytes
-rw-r--r--doc/user/project/builds/img/build_artifacts_builds_page.pngbin55625 -> 22022 bytes
-rw-r--r--doc/user/project/builds/img/build_artifacts_pipelines_page.pngbin73038 -> 28339 bytes
-rw-r--r--doc/user/project/builds/img/build_latest_artifacts_browser.pngbin26617 -> 10551 bytes
-rw-r--r--doc/user/project/container_registry.md16
-rw-r--r--doc/user/project/cycle_analytics.md9
-rw-r--r--doc/user/project/img/container_registry_enable.pngbin5526 -> 3057 bytes
-rw-r--r--doc/user/project/img/container_registry_panel.pngbin96315 -> 32310 bytes
-rw-r--r--doc/user/project/img/container_registry_tab.pngbin7284 -> 3800 bytes
-rw-r--r--doc/user/project/img/cycle_analytics_landing_page.pngbin66080 -> 42117 bytes
-rw-r--r--doc/user/project/img/description_templates.pngbin20444 -> 7903 bytes
-rw-r--r--doc/user/project/img/issue_board.pngbin275093 -> 90664 bytes
-rw-r--r--doc/user/project/img/issue_board_add_list.pngbin22391 -> 23632 bytes
-rw-r--r--doc/user/project/img/issue_board_search_backlog.pngbin25948 -> 9769 bytes
-rw-r--r--doc/user/project/img/issue_board_system_notes.pngbin20637 -> 4899 bytes
-rw-r--r--doc/user/project/img/issue_board_welcome_message.pngbin78694 -> 97419 bytes
-rw-r--r--doc/user/project/img/koding_build-in-progress.pngbin70949 -> 21953 bytes
-rw-r--r--doc/user/project/img/koding_build-logs.pngbin263623 -> 91364 bytes
-rw-r--r--doc/user/project/img/koding_build-success.pngbin304666 -> 73008 bytes
-rw-r--r--doc/user/project/img/koding_commit-koding.yml.pngbin302703 -> 86043 bytes
-rw-r--r--doc/user/project/img/koding_different-stack-on-mr-try.pngbin333649 -> 93404 bytes
-rw-r--r--doc/user/project/img/koding_edit-on-ide.pngbin330880 -> 90701 bytes
-rw-r--r--doc/user/project/img/koding_enable-koding.pngbin73499 -> 20303 bytes
-rw-r--r--doc/user/project/img/koding_landing.pngbin268455 -> 81010 bytes
-rw-r--r--doc/user/project/img/koding_open-gitlab-from-koding.pngbin32559 -> 10851 bytes
-rw-r--r--doc/user/project/img/koding_run-in-ide.pngbin65465 -> 22179 bytes
-rw-r--r--doc/user/project/img/koding_run-mr-in-ide.pngbin339759 -> 93780 bytes
-rw-r--r--doc/user/project/img/koding_set-up-ide.pngbin207481 -> 54062 bytes
-rw-r--r--doc/user/project/img/koding_stack-import.pngbin500352 -> 137608 bytes
-rw-r--r--doc/user/project/img/koding_start-build.pngbin105253 -> 27926 bytes
-rw-r--r--doc/user/project/img/labels_assign_label_in_new_issue.pngbin31126 -> 11636 bytes
-rw-r--r--doc/user/project/img/labels_assign_label_sidebar.pngbin31537 -> 11767 bytes
-rw-r--r--doc/user/project/img/labels_assign_label_sidebar_saved.pngbin28396 -> 9741 bytes
-rw-r--r--doc/user/project/img/labels_default.pngbin80403 -> 32030 bytes
-rw-r--r--doc/user/project/img/labels_description_tooltip.pngbin22585 -> 8538 bytes
-rw-r--r--doc/user/project/img/labels_filter.pngbin81536 -> 31931 bytes
-rw-r--r--doc/user/project/img/labels_filter_by_priority.pngbin60849 -> 23969 bytes
-rw-r--r--doc/user/project/img/labels_generate.pngbin31608 -> 13628 bytes
-rw-r--r--doc/user/project/img/labels_new_label.pngbin43265 -> 16787 bytes
-rw-r--r--doc/user/project/img/labels_new_label_on_the_fly.pngbin10416 -> 4625 bytes
-rw-r--r--doc/user/project/img/labels_new_label_on_the_fly_create.pngbin16151 -> 6389 bytes
-rw-r--r--doc/user/project/img/labels_prioritize.pngbin108751 -> 38185 bytes
-rw-r--r--doc/user/project/img/labels_subscribe.pngbin11536 -> 5336 bytes
-rw-r--r--doc/user/project/img/mitmproxy-docker.pngbin407004 -> 142591 bytes
-rw-r--r--doc/user/project/img/project_settings_list.pngbin11404 -> 5919 bytes
-rw-r--r--doc/user/project/img/protected_branches_choose_branch.pngbin20659 -> 7009 bytes
-rw-r--r--doc/user/project/img/protected_branches_devs_can_push.pngbin19312 -> 8302 bytes
-rw-r--r--doc/user/project/img/protected_branches_error_ui.pngbin37750 -> 13125 bytes
-rw-r--r--doc/user/project/img/protected_branches_list.pngbin16223 -> 6937 bytes
-rw-r--r--doc/user/project/img/protected_branches_matches.pngbin32145 -> 12028 bytes
-rw-r--r--doc/user/project/img/protected_branches_page.pngbin17839 -> 7205 bytes
-rw-r--r--doc/user/project/issue_board.md2
-rw-r--r--doc/user/project/merge_requests.md10
-rw-r--r--doc/user/project/merge_requests/img/cherry_pick_changes_commit.pngbin304098 -> 141744 bytes
-rw-r--r--doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.pngbin264883 -> 111488 bytes
-rw-r--r--doc/user/project/merge_requests/img/cherry_pick_changes_mr.pngbin212267 -> 93870 bytes
-rw-r--r--doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.pngbin186597 -> 86650 bytes
-rw-r--r--doc/user/project/merge_requests/img/commit_compare.pngbin65010 -> 33385 bytes
-rw-r--r--doc/user/project/merge_requests/img/conflict_section.pngbin247537 -> 72815 bytes
-rw-r--r--doc/user/project/merge_requests/img/discussion_view.pngbin292754 -> 73821 bytes
-rw-r--r--doc/user/project/merge_requests/img/discussions_resolved.pngbin12840 -> 4152 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_request_diff.pngbin69394 -> 26650 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_request_widget.pngbin32292 -> 11039 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.pngbin68769 -> 39796 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.pngbin11136 -> 5251 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.pngbin17552 -> 12063 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_when_build_succeeds_status.pngbin82655 -> 48458 bytes
-rw-r--r--doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved.pngbin24693 -> 17888 bytes
-rw-r--r--doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved_msg.pngbin6940 -> 4962 bytes
-rw-r--r--doc/user/project/merge_requests/img/preview_issue_for_discussions.pngbin0 -> 178361 bytes
-rw-r--r--doc/user/project/merge_requests/img/resolve_comment_button.pngbin14075 -> 4722 bytes
-rw-r--r--doc/user/project/merge_requests/img/resolve_discussion_button.pngbin18405 -> 4683 bytes
-rw-r--r--doc/user/project/merge_requests/img/revert_changes_commit.pngbin233750 -> 95655 bytes
-rw-r--r--doc/user/project/merge_requests/img/revert_changes_commit_modal.pngbin205046 -> 88824 bytes
-rw-r--r--doc/user/project/merge_requests/img/revert_changes_mr.pngbin241051 -> 104972 bytes
-rw-r--r--doc/user/project/merge_requests/img/revert_changes_mr_modal.pngbin211022 -> 93536 bytes
-rw-r--r--doc/user/project/merge_requests/img/versions.pngbin171413 -> 55703 bytes
-rw-r--r--doc/user/project/merge_requests/img/versions_compare.pngbin68722 -> 24886 bytes
-rw-r--r--doc/user/project/merge_requests/img/versions_dropdown.pngbin60587 -> 21547 bytes
-rw-r--r--doc/user/project/merge_requests/img/versions_system_note.pngbin18731 -> 7136 bytes
-rw-r--r--doc/user/project/merge_requests/img/wip_blocked_accept_button.pngbin32720 -> 18606 bytes
-rw-r--r--doc/user/project/merge_requests/img/wip_mark_as_wip.pngbin21640 -> 11396 bytes
-rw-r--r--doc/user/project/merge_requests/img/wip_unmark_as_wip.pngbin16606 -> 8565 bytes
-rw-r--r--doc/user/project/merge_requests/merge_request_discussion_resolution.md21
-rw-r--r--doc/user/project/merge_requests/merge_when_build_succeeds.md47
-rw-r--r--doc/user/project/merge_requests/merge_when_pipeline_succeeds.md46
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md130
-rw-r--r--doc/user/project/pipelines/img/pipelines_settings_badges.pngbin56166 -> 21137 bytes
-rw-r--r--doc/user/project/pipelines/img/pipelines_settings_test_coverage.pngbin4212 -> 2603 bytes
-rw-r--r--doc/user/project/pipelines/img/pipelines_test_coverage_build.pngbin9953 -> 4481 bytes
-rw-r--r--doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.pngbin14502 -> 6391 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_branch_dropdown.pngbin20436 -> 10386 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_branch_from_issue.pngbin4728 -> 2720 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_branch_page.pngbin11245 -> 6034 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_directory_dialog.pngbin13339 -> 7323 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_directory_dropdown.pngbin20007 -> 9918 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_file_dropdown.pngbin20680 -> 10233 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_file_editor.pngbin66261 -> 38068 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_push_widget.pngbin7076 -> 3395 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_tag_dropdown.pngbin20080 -> 9796 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_tag_page.pngbin36610 -> 21835 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_start_new_merge_request.pngbin8596 -> 4060 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_template_dropdown_buttons.pngbin14131 -> 5634 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_template_dropdown_first_file.pngbin25748 -> 8846 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_template_dropdown_mit_license.pngbin85413 -> 30924 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_upload_file_dialog.pngbin21502 -> 12558 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_upload_file_dropdown.pngbin20651 -> 10291 bytes
-rw-r--r--doc/user/project/settings/img/import_export_download_export.pngbin85600 -> 24482 bytes
-rw-r--r--doc/user/project/settings/img/import_export_export_button.pngbin84637 -> 24122 bytes
-rw-r--r--doc/user/project/settings/img/import_export_mail_link.pngbin44012 -> 13496 bytes
-rw-r--r--doc/user/project/settings/img/import_export_new_project.pngbin43574 -> 13083 bytes
-rw-r--r--doc/user/project/settings/img/import_export_select_file.pngbin46292 -> 13713 bytes
-rw-r--r--doc/user/project/settings/img/settings_edit_button.pngbin19392 -> 6901 bytes
-rw-r--r--doc/web_hooks/ssl.pngbin39120 -> 27799 bytes
-rw-r--r--doc/web_hooks/web_hooks.md133
-rw-r--r--doc/workflow/README.md4
-rw-r--r--doc/workflow/add-user/img/access_requests_management.pngbin15686 -> 11018 bytes
-rw-r--r--doc/workflow/add-user/img/add_new_user_to_project_settings.pngbin18149 -> 11046 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_email_accept.pngbin22877 -> 16890 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_email_ready.pngbin40207 -> 28171 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_email_search.pngbin45798 -> 29628 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_give_permissions.pngbin56380 -> 36619 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_import_members_from_another_project.pngbin38778 -> 25343 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_imported_members.pngbin37835 -> 25398 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_list_members.pngbin24337 -> 16916 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_members_menu.pngbin42224 -> 28994 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_search_people.pngbin39844 -> 25368 bytes
-rw-r--r--doc/workflow/add-user/img/request_access_button.pngbin36588 -> 25281 bytes
-rw-r--r--doc/workflow/add-user/img/withdraw_access_request_button.pngbin37960 -> 26135 bytes
-rw-r--r--doc/workflow/award_emoji.pngbin9939 -> 5268 bytes
-rw-r--r--doc/workflow/ci_mr.pngbin29571 -> 12034 bytes
-rw-r--r--doc/workflow/close_issue_mr.pngbin82595 -> 42108 bytes
-rw-r--r--doc/workflow/environment_branches.pngbin20745 -> 12364 bytes
-rw-r--r--doc/workflow/forking/branch_select.pngbin27299 -> 15424 bytes
-rw-r--r--doc/workflow/forking/merge_request.pngbin31560 -> 16332 bytes
-rw-r--r--doc/workflow/four_stages.pngbin10003 -> 7124 bytes
-rw-r--r--doc/workflow/git_pull.pngbin94405 -> 28749 bytes
-rw-r--r--doc/workflow/gitdashflow.pngbin131491 -> 68177 bytes
-rw-r--r--doc/workflow/github_flow.pngbin10251 -> 6173 bytes
-rw-r--r--doc/workflow/gitlab_flow.md2
-rw-r--r--doc/workflow/gitlab_flow.pngbin70871 -> 47432 bytes
-rw-r--r--doc/workflow/good_commit.pngbin13131 -> 8742 bytes
-rw-r--r--doc/workflow/groups/access_requests_management.pngbin15829 -> 11186 bytes
-rw-r--r--doc/workflow/groups/add_member_to_group.pngbin78060 -> 35724 bytes
-rw-r--r--doc/workflow/groups/group_dashboard.pngbin59446 -> 28155 bytes
-rw-r--r--doc/workflow/groups/group_with_two_projects.pngbin73101 -> 34462 bytes
-rw-r--r--doc/workflow/groups/max_access_level.pngbin74947 -> 34718 bytes
-rw-r--r--doc/workflow/groups/new_group_button.pngbin108482 -> 49708 bytes
-rw-r--r--doc/workflow/groups/new_group_form.pngbin58860 -> 27263 bytes
-rw-r--r--doc/workflow/groups/other_group_sees_shared_project.pngbin64447 -> 30182 bytes
-rw-r--r--doc/workflow/groups/override_access_level.pngbin90122 -> 40993 bytes
-rw-r--r--doc/workflow/groups/project_members_via_group.pngbin86260 -> 39532 bytes
-rw-r--r--doc/workflow/groups/request_access_button.pngbin49067 -> 35917 bytes
-rw-r--r--doc/workflow/groups/share_project_with_groups.pngbin65633 -> 30307 bytes
-rw-r--r--doc/workflow/groups/transfer_project.pngbin92115 -> 43502 bytes
-rw-r--r--doc/workflow/groups/withdraw_access_request_button.pngbin49941 -> 36413 bytes
-rw-r--r--doc/workflow/img/award_emoji_comment_awarded.pngbin64317 -> 19159 bytes
-rw-r--r--doc/workflow/img/award_emoji_comment_picker.pngbin250861 -> 72883 bytes
-rw-r--r--doc/workflow/img/award_emoji_select.pngbin49296 -> 23779 bytes
-rw-r--r--doc/workflow/img/award_emoji_votes_least_popular.pngbin116715 -> 50191 bytes
-rw-r--r--doc/workflow/img/award_emoji_votes_most_popular.pngbin108775 -> 48342 bytes
-rw-r--r--doc/workflow/img/award_emoji_votes_sort_options.pngbin131659 -> 57145 bytes
-rw-r--r--doc/workflow/img/file_finder_find_button.pngbin25458 -> 14567 bytes
-rw-r--r--doc/workflow/img/file_finder_find_file.pngbin35114 -> 19478 bytes
-rw-r--r--doc/workflow/img/forking_workflow_choose_namespace.pngbin59114 -> 26275 bytes
-rw-r--r--doc/workflow/img/forking_workflow_fork_button.pngbin20750 -> 12973 bytes
-rw-r--r--doc/workflow/img/forking_workflow_path_taken_error.pngbin17978 -> 10103 bytes
-rw-r--r--doc/workflow/img/new_branch_from_issue.pngbin54607 -> 33584 bytes
-rw-r--r--doc/workflow/img/todo_list_item.pngbin58912 -> 18777 bytes
-rw-r--r--doc/workflow/img/todos_add_todo_sidebar.pngbin120265 -> 42360 bytes
-rw-r--r--doc/workflow/img/todos_icon.pngbin3843 -> 1341 bytes
-rw-r--r--doc/workflow/img/todos_index.pngbin152040 -> 63372 bytes
-rw-r--r--doc/workflow/img/todos_mark_done_sidebar.pngbin121303 -> 42317 bytes
-rw-r--r--doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.pngbin30083 -> 0 bytes
-rw-r--r--doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.pngbin16502 -> 0 bytes
-rw-r--r--doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.pngbin46606 -> 0 bytes
-rw-r--r--doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.pngbin15288 -> 0 bytes
-rw-r--r--doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.pngbin30266 -> 17744 bytes
-rw-r--r--doc/workflow/importing/fogbugz_importer/fogbugz_import_login.pngbin20797 -> 13751 bytes
-rw-r--r--doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.pngbin20526 -> 12289 bytes
-rw-r--r--doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.pngbin34836 -> 20905 bytes
-rw-r--r--doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.pngbin77208 -> 51238 bytes
-rw-r--r--doc/workflow/importing/gitlab_importer/importer.pngbin18366 -> 12864 bytes
-rw-r--r--doc/workflow/importing/gitlab_importer/new_project_page.pngbin33589 -> 21251 bytes
-rw-r--r--doc/workflow/importing/img/bitbucket_import_grant_access.pngbin0 -> 7248 bytes
-rw-r--r--doc/workflow/importing/img/bitbucket_import_new_project.pngbin0 -> 1316 bytes
-rw-r--r--doc/workflow/importing/img/bitbucket_import_select_project.pngbin0 -> 8688 bytes
-rw-r--r--doc/workflow/importing/img/import_projects_from_github_importer.pngbin65288 -> 17953 bytes
-rw-r--r--doc/workflow/importing/img/import_projects_from_github_new_project_page.pngbin24911 -> 0 bytes
-rw-r--r--doc/workflow/importing/img/import_projects_from_github_select_auth_method.pngbin42043 -> 17612 bytes
-rw-r--r--doc/workflow/importing/img/import_projects_from_new_project_page.pngbin0 -> 36821 bytes
-rw-r--r--doc/workflow/importing/import_projects_from_bitbucket.md61
-rw-r--r--doc/workflow/importing/import_projects_from_github.md2
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md12
-rw-r--r--doc/workflow/merge_commits.pngbin22181 -> 7564 bytes
-rw-r--r--doc/workflow/merge_request.pngbin98070 -> 47240 bytes
-rw-r--r--doc/workflow/merge_when_build_succeeds.md2
-rw-r--r--doc/workflow/messy_flow.pngbin19314 -> 11665 bytes
-rw-r--r--doc/workflow/milestones/form.pngbin84872 -> 40414 bytes
-rw-r--r--doc/workflow/milestones/group_form.pngbin74429 -> 35820 bytes
-rw-r--r--doc/workflow/mr_inline_comments.pngbin117313 -> 52519 bytes
-rw-r--r--doc/workflow/notifications.md3
-rw-r--r--doc/workflow/notifications/settings.pngbin59256 -> 37542 bytes
-rw-r--r--doc/workflow/production_branch.pngbin10946 -> 7264 bytes
-rw-r--r--doc/workflow/rebase.pngbin68976 -> 29009 bytes
-rw-r--r--doc/workflow/release_branches.pngbin22163 -> 12746 bytes
-rw-r--r--doc/workflow/releases/new_tag.pngbin87330 -> 42456 bytes
-rw-r--r--doc/workflow/releases/tags.pngbin93016 -> 44666 bytes
-rw-r--r--doc/workflow/remove_checkbox.pngbin12339 -> 6904 bytes
-rw-r--r--doc/workflow/todos.md10
-rw-r--r--features/abuse_report.feature17
-rw-r--r--features/admin/abuse_report.feature8
-rw-r--r--features/admin/active_tab.feature54
-rw-r--r--features/admin/hooks.feature9
-rw-r--r--features/admin/logs.feature8
-rw-r--r--features/admin/settings.feature19
-rw-r--r--features/admin/spam_logs.feature8
-rw-r--r--features/project/wiki.feature5
-rw-r--r--features/snippet_search.feature20
-rw-r--r--features/snippets/discover.feature13
-rw-r--r--features/steps/abuse_reports.rb32
-rw-r--r--features/steps/admin/abuse_reports.rb15
-rw-r--r--features/steps/admin/active_tab.rb41
-rw-r--r--features/steps/admin/hooks.rb15
-rw-r--r--features/steps/admin/logs.rb11
-rw-r--r--features/steps/admin/settings.rb62
-rw-r--r--features/steps/admin/spam_logs.rb28
-rw-r--r--features/steps/group/members.rb7
-rw-r--r--features/steps/group/milestones.rb2
-rw-r--r--features/steps/project/issues/issues.rb2
-rw-r--r--features/steps/project/labels.rb2
-rw-r--r--features/steps/project/merge_requests.rb20
-rw-r--r--features/steps/project/snippets.rb2
-rw-r--r--features/steps/project/source/markdown_render.rb6
-rw-r--r--features/steps/project/team_management.rb8
-rw-r--r--features/steps/project/wiki.rb16
-rw-r--r--features/steps/shared/authentication.rb2
-rw-r--r--features/steps/shared/diff_note.rb5
-rw-r--r--features/steps/shared/issuable.rb8
-rw-r--r--features/steps/shared/markdown.rb2
-rw-r--r--features/steps/shared/note.rb4
-rw-r--r--features/steps/shared/paths.rb6
-rw-r--r--features/steps/shared/project.rb2
-rw-r--r--features/steps/shared/search.rb11
-rw-r--r--features/steps/snippet_search.rb55
-rw-r--r--features/steps/snippets/discover.rb21
-rw-r--r--features/support/capybara.rb10
-rw-r--r--lib/api/access_requests.rb7
-rw-r--r--lib/api/api.rb3
-rw-r--r--lib/api/api_guard.rb50
-rw-r--r--lib/api/award_emoji.rb5
-rw-r--r--lib/api/branches.rb28
-rw-r--r--lib/api/broadcast_messages.rb11
-rw-r--r--lib/api/builds.rb7
-rw-r--r--lib/api/commit_statuses.rb6
-rw-r--r--lib/api/commits.rb45
-rw-r--r--lib/api/deploy_keys.rb2
-rw-r--r--lib/api/deployments.rb5
-rw-r--r--lib/api/entities.rb51
-rw-r--r--lib/api/environments.rb15
-rw-r--r--lib/api/files.rb153
-rw-r--r--lib/api/groups.rb194
-rw-r--r--lib/api/helpers.rb227
-rw-r--r--lib/api/helpers/custom_validators.rb14
-rw-r--r--lib/api/helpers/internal_helpers.rb57
-rw-r--r--lib/api/helpers/members_helpers.rb2
-rw-r--r--lib/api/internal.rb38
-rw-r--r--lib/api/issues.rb290
-rw-r--r--lib/api/labels.rb11
-rw-r--r--lib/api/members.rb8
-rw-r--r--lib/api/merge_requests.rb321
-rw-r--r--lib/api/milestones.rb118
-rw-r--r--lib/api/namespaces.rb4
-rw-r--r--lib/api/notes.rb128
-rw-r--r--lib/api/notification_settings.rb7
-rw-r--r--lib/api/pagination_params.rb24
-rw-r--r--lib/api/pipelines.rb26
-rw-r--r--lib/api/project_hooks.rb22
-rw-r--r--lib/api/project_snippets.rb162
-rw-r--r--lib/api/projects.rb613
-rw-r--r--lib/api/repositories.rb95
-rw-r--r--lib/api/runners.rb120
-rw-r--r--lib/api/services.rb659
-rw-r--r--lib/api/session.rb21
-rw-r--r--lib/api/sidekiq_metrics.rb36
-rw-r--r--lib/api/snippets.rb137
-rw-r--r--lib/api/subscriptions.rb49
-rw-r--r--lib/api/system_hooks.rb2
-rw-r--r--lib/api/tags.rb4
-rw-r--r--lib/api/todos.rb10
-rw-r--r--lib/api/triggers.rb85
-rw-r--r--lib/api/users.rb518
-rw-r--r--lib/api/variables.rb7
-rw-r--r--lib/backup/manager.rb19
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb25
-rw-r--r--lib/banzai/filter/autolink_filter.rb38
-rw-r--r--lib/banzai/filter/commit_range_reference_filter.rb2
-rw-r--r--lib/banzai/filter/commit_reference_filter.rb2
-rw-r--r--lib/banzai/filter/label_reference_filter.rb54
-rw-r--r--lib/banzai/filter/math_filter.rb51
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb20
-rw-r--r--lib/banzai/filter/relative_link_filter.rb14
-rw-r--r--lib/banzai/filter/table_of_contents_filter.rb8
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb1
-rw-r--r--lib/banzai/reference_parser/base_parser.rb16
-rw-r--r--lib/banzai/reference_parser/commit_parser.rb6
-rw-r--r--lib/banzai/reference_parser/commit_range_parser.rb6
-rw-r--r--lib/banzai/reference_parser/external_issue_parser.rb6
-rw-r--r--lib/banzai/reference_parser/label_parser.rb6
-rw-r--r--lib/banzai/reference_parser/merge_request_parser.rb6
-rw-r--r--lib/banzai/reference_parser/milestone_parser.rb6
-rw-r--r--lib/banzai/reference_parser/snippet_parser.rb6
-rw-r--r--lib/banzai/reference_parser/user_parser.rb34
-rw-r--r--lib/bitbucket/client.rb58
-rw-r--r--lib/bitbucket/collection.rb21
-rw-r--r--lib/bitbucket/connection.rb69
-rw-r--r--lib/bitbucket/error/unauthorized.rb6
-rw-r--r--lib/bitbucket/page.rb34
-rw-r--r--lib/bitbucket/paginator.rb36
-rw-r--r--lib/bitbucket/representation/base.rb17
-rw-r--r--lib/bitbucket/representation/comment.rb27
-rw-r--r--lib/bitbucket/representation/issue.rb53
-rw-r--r--lib/bitbucket/representation/pull_request.rb65
-rw-r--r--lib/bitbucket/representation/pull_request_comment.rb39
-rw-r--r--lib/bitbucket/representation/repo.rb67
-rw-r--r--lib/bitbucket/representation/user.rb9
-rw-r--r--lib/ci/api/entities.rb6
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb2
-rw-r--r--lib/constraints/group_url_constrainer.rb18
-rw-r--r--lib/constraints/namespace_url_constrainer.rb24
-rw-r--r--lib/constraints/project_url_constrainer.rb13
-rw-r--r--lib/constraints/user_url_constrainer.rb8
-rw-r--r--lib/email_template_interceptor.rb13
-rw-r--r--lib/event_filter.rb38
-rw-r--r--lib/gitlab/allowable.rb7
-rw-r--r--lib/gitlab/asciidoc.rb29
-rw-r--r--lib/gitlab/auth.rb25
-rw-r--r--lib/gitlab/badge/build/status.rb4
-rw-r--r--lib/gitlab/bitbucket_import.rb6
-rw-r--r--lib/gitlab/bitbucket_import/client.rb142
-rw-r--r--lib/gitlab/bitbucket_import/importer.rb242
-rw-r--r--lib/gitlab/bitbucket_import/key_adder.rb24
-rw-r--r--lib/gitlab/bitbucket_import/key_deleter.rb23
-rw-r--r--lib/gitlab/bitbucket_import/project_creator.rb21
-rw-r--r--lib/gitlab/chat_commands/base_command.rb47
-rw-r--r--lib/gitlab/chat_commands/command.rb63
-rw-r--r--lib/gitlab/chat_commands/deploy.rb57
-rw-r--r--lib/gitlab/chat_commands/issue_command.rb13
-rw-r--r--lib/gitlab/chat_commands/issue_create.rb26
-rw-r--r--lib/gitlab/chat_commands/issue_search.rb17
-rw-r--r--lib/gitlab/chat_commands/issue_show.rb17
-rw-r--r--lib/gitlab/chat_commands/result.rb5
-rw-r--r--lib/gitlab/chat_name_token.rb45
-rw-r--r--lib/gitlab/ci/build/credentials/base.rb13
-rw-r--r--lib/gitlab/ci/build/credentials/factory.rb27
-rw-r--r--lib/gitlab/ci/build/credentials/registry.rb24
-rw-r--r--lib/gitlab/ci/config.rb43
-rw-r--r--lib/gitlab/ci/config/entry/artifacts.rb35
-rw-r--r--lib/gitlab/ci/config/entry/attributable.rb23
-rw-r--r--lib/gitlab/ci/config/entry/boolean.rb18
-rw-r--r--lib/gitlab/ci/config/entry/cache.rb29
-rw-r--r--lib/gitlab/ci/config/entry/commands.rb33
-rw-r--r--lib/gitlab/ci/config/entry/configurable.rb76
-rw-r--r--lib/gitlab/ci/config/entry/environment.rb82
-rw-r--r--lib/gitlab/ci/config/entry/factory.rb73
-rw-r--r--lib/gitlab/ci/config/entry/global.rb72
-rw-r--r--lib/gitlab/ci/config/entry/hidden.rb22
-rw-r--r--lib/gitlab/ci/config/entry/image.rb18
-rw-r--r--lib/gitlab/ci/config/entry/job.rb140
-rw-r--r--lib/gitlab/ci/config/entry/jobs.rb52
-rw-r--r--lib/gitlab/ci/config/entry/key.rb18
-rw-r--r--lib/gitlab/ci/config/entry/legacy_validation_helpers.rb51
-rw-r--r--lib/gitlab/ci/config/entry/node.rb83
-rw-r--r--lib/gitlab/ci/config/entry/paths.rb18
-rw-r--r--lib/gitlab/ci/config/entry/script.rb18
-rw-r--r--lib/gitlab/ci/config/entry/services.rb18
-rw-r--r--lib/gitlab/ci/config/entry/stage.rb22
-rw-r--r--lib/gitlab/ci/config/entry/stages.rb22
-rw-r--r--lib/gitlab/ci/config/entry/trigger.rb26
-rw-r--r--lib/gitlab/ci/config/entry/undefined.rb36
-rw-r--r--lib/gitlab/ci/config/entry/unspecified.rb19
-rw-r--r--lib/gitlab/ci/config/entry/validatable.rb27
-rw-r--r--lib/gitlab/ci/config/entry/validator.rb42
-rw-r--r--lib/gitlab/ci/config/entry/validators.rb82
-rw-r--r--lib/gitlab/ci/config/entry/variables.rb22
-rw-r--r--lib/gitlab/ci/config/node/artifacts.rb35
-rw-r--r--lib/gitlab/ci/config/node/attributable.rb23
-rw-r--r--lib/gitlab/ci/config/node/boolean.rb18
-rw-r--r--lib/gitlab/ci/config/node/cache.rb29
-rw-r--r--lib/gitlab/ci/config/node/commands.rb33
-rw-r--r--lib/gitlab/ci/config/node/configurable.rb78
-rw-r--r--lib/gitlab/ci/config/node/entry.rb83
-rw-r--r--lib/gitlab/ci/config/node/environment.rb82
-rw-r--r--lib/gitlab/ci/config/node/factory.rb73
-rw-r--r--lib/gitlab/ci/config/node/global.rb72
-rw-r--r--lib/gitlab/ci/config/node/hidden.rb22
-rw-r--r--lib/gitlab/ci/config/node/image.rb18
-rw-r--r--lib/gitlab/ci/config/node/job.rb140
-rw-r--r--lib/gitlab/ci/config/node/jobs.rb52
-rw-r--r--lib/gitlab/ci/config/node/key.rb18
-rw-r--r--lib/gitlab/ci/config/node/legacy_validation_helpers.rb51
-rw-r--r--lib/gitlab/ci/config/node/paths.rb18
-rw-r--r--lib/gitlab/ci/config/node/script.rb18
-rw-r--r--lib/gitlab/ci/config/node/services.rb18
-rw-r--r--lib/gitlab/ci/config/node/stage.rb22
-rw-r--r--lib/gitlab/ci/config/node/stages.rb22
-rw-r--r--lib/gitlab/ci/config/node/trigger.rb26
-rw-r--r--lib/gitlab/ci/config/node/undefined.rb38
-rw-r--r--lib/gitlab/ci/config/node/unspecified.rb19
-rw-r--r--lib/gitlab/ci/config/node/validatable.rb27
-rw-r--r--lib/gitlab/ci/config/node/validator.rb42
-rw-r--r--lib/gitlab/ci/config/node/validators.rb82
-rw-r--r--lib/gitlab/ci/config/node/variables.rb22
-rw-r--r--lib/gitlab/ci/status/build/cancelable.rb37
-rw-r--r--lib/gitlab/ci/status/build/common.rb19
-rw-r--r--lib/gitlab/ci/status/build/factory.rb18
-rw-r--r--lib/gitlab/ci/status/build/play.rb53
-rw-r--r--lib/gitlab/ci/status/build/retryable.rb37
-rw-r--r--lib/gitlab/ci/status/build/stop.rb49
-rw-r--r--lib/gitlab/ci/status/canceled.rb19
-rw-r--r--lib/gitlab/ci/status/core.rb69
-rw-r--r--lib/gitlab/ci/status/created.rb19
-rw-r--r--lib/gitlab/ci/status/extended.rb15
-rw-r--r--lib/gitlab/ci/status/factory.rb47
-rw-r--r--lib/gitlab/ci/status/failed.rb19
-rw-r--r--lib/gitlab/ci/status/pending.rb19
-rw-r--r--lib/gitlab/ci/status/pipeline/common.rb23
-rw-r--r--lib/gitlab/ci/status/pipeline/factory.rb17
-rw-r--r--lib/gitlab/ci/status/pipeline/success_with_warnings.rb31
-rw-r--r--lib/gitlab/ci/status/running.rb19
-rw-r--r--lib/gitlab/ci/status/skipped.rb19
-rw-r--r--lib/gitlab/ci/status/stage/common.rb24
-rw-r--r--lib/gitlab/ci/status/stage/factory.rb13
-rw-r--r--lib/gitlab/ci/status/success.rb19
-rw-r--r--lib/gitlab/contributions_calendar.rb74
-rw-r--r--lib/gitlab/current_settings.rb5
-rw-r--r--lib/gitlab/cycle_analytics/base_event.rb57
-rw-r--r--lib/gitlab/cycle_analytics/code_event.rb28
-rw-r--r--lib/gitlab/cycle_analytics/events.rb38
-rw-r--r--lib/gitlab/cycle_analytics/events_query.rb37
-rw-r--r--lib/gitlab/cycle_analytics/issue_allowed.rb9
-rw-r--r--lib/gitlab/cycle_analytics/issue_event.rb27
-rw-r--r--lib/gitlab/cycle_analytics/merge_request_allowed.rb9
-rw-r--r--lib/gitlab/cycle_analytics/metrics_fetcher.rb60
-rw-r--r--lib/gitlab/cycle_analytics/metrics_tables.rb37
-rw-r--r--lib/gitlab/cycle_analytics/permissions.rb44
-rw-r--r--lib/gitlab/cycle_analytics/plan_event.rb46
-rw-r--r--lib/gitlab/cycle_analytics/production_event.rb26
-rw-r--r--lib/gitlab/cycle_analytics/review_event.rb25
-rw-r--r--lib/gitlab/cycle_analytics/staging_event.rb31
-rw-r--r--lib/gitlab/cycle_analytics/test_event.rb13
-rw-r--r--lib/gitlab/cycle_analytics/updater.rb30
-rw-r--r--lib/gitlab/data_builder/pipeline.rb2
-rw-r--r--lib/gitlab/database/date_time.rb28
-rw-r--r--lib/gitlab/diff/file.rb15
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff.rb8
-rw-r--r--lib/gitlab/ee_compat_check.rb160
-rw-r--r--lib/gitlab/email/html_parser.rb34
-rw-r--r--lib/gitlab/email/reply_parser.rb21
-rw-r--r--lib/gitlab/file_detector.rb63
-rw-r--r--lib/gitlab/gfm/uploads_rewriter.rb19
-rw-r--r--lib/gitlab/git_access.rb95
-rw-r--r--lib/gitlab/git_access_wiki.rb8
-rw-r--r--lib/gitlab/github_import/branch_formatter.rb2
-rw-r--r--lib/gitlab/github_import/importer.rb50
-rw-r--r--lib/gitlab/github_import/issue_formatter.rb4
-rw-r--r--lib/gitlab/gon_helper.rb2
-rw-r--r--lib/gitlab/identifier.rb6
-rw-r--r--lib/gitlab/ldap/adapter.rb4
-rw-r--r--lib/gitlab/ldap/authentication.rb6
-rw-r--r--lib/gitlab/ldap/config.rb69
-rw-r--r--lib/gitlab/mail_room.rb1
-rw-r--r--lib/gitlab/middleware/multipart.rb99
-rw-r--r--lib/gitlab/o_auth/user.rb4
-rw-r--r--lib/gitlab/project_search_results.rb75
-rw-r--r--lib/gitlab/regex.rb48
-rw-r--r--lib/gitlab/routing.rb6
-rw-r--r--lib/gitlab/search_results.rb4
-rw-r--r--lib/gitlab/sidekiq_throttler.rb23
-rw-r--r--lib/gitlab/sql/union.rb4
-rw-r--r--lib/gitlab/url_builder.rb2
-rw-r--r--lib/gitlab/workhorse.rb6
-rw-r--r--lib/mattermost/presenter.rb131
-rw-r--r--lib/mattermost/session.rb115
-rw-r--r--lib/omniauth/strategies/bitbucket.rb41
-rw-r--r--lib/rouge/lexers/math.rb21
-rw-r--r--lib/tasks/gitlab/check.rake39
-rw-r--r--lib/tasks/gitlab/cleanup.rake23
-rw-r--r--lib/tasks/gitlab/dev.rake23
-rw-r--r--lib/tasks/gitlab/helpers.rake8
-rw-r--r--lib/tasks/gitlab/shell.rake42
-rw-r--r--lib/tasks/gitlab/task_helpers.rake140
-rw-r--r--lib/tasks/gitlab/task_helpers.rb190
-rw-r--r--lib/tasks/gitlab/workhorse.rake23
-rw-r--r--lib/tasks/migrate/setup_postgresql.rake4
-rw-r--r--package.json15
-rw-r--r--public/404.html14
-rw-r--r--public/422.html14
-rw-r--r--public/500.html14
-rw-r--r--public/502.html14
-rw-r--r--public/503.html14
-rwxr-xr-xscripts/lint-doc.sh2
-rwxr-xr-xscripts/prepare_build.sh9
-rw-r--r--spec/config/mail_room_spec.rb1
-rw-r--r--spec/controllers/admin/impersonations_controller_spec.rb28
-rw-r--r--spec/controllers/application_controller_spec.rb23
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb43
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb2
-rw-r--r--spec/controllers/groups/labels_controller_spec.rb22
-rw-r--r--spec/controllers/help_controller_spec.rb36
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb52
-rw-r--r--spec/controllers/profiles/personal_access_tokens_spec.rb49
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb49
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb2
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb76
-rw-r--r--spec/controllers/projects/cycle_analytics_controller_spec.rb43
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb33
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb58
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb60
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb30
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb29
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb2
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb2
-rw-r--r--spec/controllers/projects/releases_controller_spec.rb55
-rw-r--r--spec/controllers/projects/todo_controller_spec.rb32
-rw-r--r--spec/controllers/search_controller_spec.rb61
-rw-r--r--spec/controllers/sent_notifications_controller_spec.rb18
-rw-r--r--spec/controllers/sessions_controller_spec.rb1
-rw-r--r--spec/controllers/snippets_controller_spec.rb22
-rw-r--r--spec/db/production/settings.rb16
-rw-r--r--spec/factories/chat_names.rb16
-rw-r--r--spec/factories/ci/builds.rb17
-rw-r--r--spec/factories/ci/pipelines.rb28
-rw-r--r--spec/factories/ci/stages.rb13
-rw-r--r--spec/factories/commit_statuses.rb4
-rw-r--r--spec/factories/deployments.rb3
-rw-r--r--spec/factories/environments.rb28
-rw-r--r--spec/factories/groups.rb8
-rw-r--r--spec/factories/notes.rb2
-rw-r--r--spec/factories/personal_access_tokens.rb1
-rw-r--r--spec/factories/projects.rb27
-rw-r--r--spec/factories/subscriptions.rb7
-rw-r--r--spec/features/abuse_report_spec.rb24
-rw-r--r--spec/features/admin/admin_abuse_reports_spec.rb16
-rw-r--r--spec/features/admin/admin_active_tab_spec.rb90
-rw-r--r--spec/features/admin/admin_browse_spam_logs_spec.rb22
-rw-r--r--spec/features/admin/admin_browses_logs_spec.rb15
-rw-r--r--spec/features/admin/admin_groups_spec.rb35
-rw-r--r--spec/features/admin/admin_hooks_spec.rb15
-rw-r--r--spec/features/admin/admin_projects_spec.rb6
-rw-r--r--spec/features/admin/admin_settings_spec.rb53
-rw-r--r--spec/features/admin/admin_users_spec.rb28
-rw-r--r--spec/features/boards/boards_spec.rb20
-rw-r--r--spec/features/boards/new_issue_spec.rb2
-rw-r--r--spec/features/boards/sidebar_spec.rb4
-rw-r--r--spec/features/calendar_spec.rb41
-rw-r--r--spec/features/commits_spec.rb21
-rw-r--r--spec/features/dashboard/datetime_on_tooltips_spec.rb2
-rw-r--r--spec/features/dashboard/issuables_counter_spec.rb44
-rw-r--r--spec/features/environment_spec.rb161
-rw-r--r--spec/features/environments_spec.rb265
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb14
-rw-r--r--spec/features/global_search_spec.rb28
-rw-r--r--spec/features/groups/issues_spec.rb8
-rw-r--r--spec/features/groups/labels/edit_spec.rb21
-rw-r--r--spec/features/groups/members/last_owner_cannot_leave_group_spec.rb4
-rw-r--r--spec/features/groups/members/member_leaves_group_spec.rb2
-rw-r--r--spec/features/groups/members/owner_manages_access_requests_spec.rb2
-rw-r--r--spec/features/groups/members/sorting_spec.rb98
-rw-r--r--spec/features/groups/members/user_requests_access_spec.rb4
-rw-r--r--spec/features/groups/merge_requests_spec.rb36
-rw-r--r--spec/features/groups_spec.rb8
-rw-r--r--spec/features/help_pages_spec.rb35
-rw-r--r--spec/features/issues/award_emoji_spec.rb93
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb42
-rw-r--r--spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb76
-rw-r--r--spec/features/issues/filter_issues_spec.rb10
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb100
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb35
-rw-r--r--spec/features/issues/move_spec.rb8
-rw-r--r--spec/features/issues/new_branch_button_spec.rb12
-rw-r--r--spec/features/issues/todo_spec.rb8
-rw-r--r--spec/features/issues_spec.rb5
-rw-r--r--spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb (renamed from spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions.rb)0
-rw-r--r--spec/features/merge_requests/closes_issues_spec.rb55
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb2
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb19
-rw-r--r--spec/features/merge_requests/deleted_source_branch_spec.rb33
-rw-r--r--spec/features/merge_requests/diff_notes_resolve_spec.rb2
-rw-r--r--spec/features/merge_requests/merge_commit_message_toggle_spec.rb74
-rw-r--r--spec/features/merge_requests/merge_request_versions_spec.rb7
-rw-r--r--spec/features/merge_requests/merge_when_build_succeeds_spec.rb108
-rw-r--r--spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb109
-rw-r--r--spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb105
-rw-r--r--spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb132
-rw-r--r--spec/features/merge_requests/target_branch_spec.rb41
-rw-r--r--spec/features/merge_requests/toggle_whitespace_changes_spec.rb (renamed from spec/features/merge_requests/toggle_whitespace_changes.rb)0
-rw-r--r--spec/features/milestone_spec.rb5
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb2
-rw-r--r--spec/features/participants_autocomplete_spec.rb7
-rw-r--r--spec/features/profiles/chat_names_spec.rb77
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb25
-rw-r--r--spec/features/profiles/preferences_spec.rb2
-rw-r--r--spec/features/projects/blobs/edit_spec.rb45
-rw-r--r--spec/features/projects/builds_spec.rb232
-rw-r--r--spec/features/projects/commits/cherry_pick_spec.rb2
-rw-r--r--spec/features/projects/features_visibility_spec.rb38
-rw-r--r--spec/features/projects/files/creating_a_file_spec.rb44
-rw-r--r--spec/features/projects/gfm_autocomplete_load_spec.rb4
-rw-r--r--spec/features/projects/guest_navigation_menu_spec.rb4
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin681774 -> 679415 bytes
-rw-r--r--spec/features/projects/labels/subscription_spec.rb74
-rw-r--r--spec/features/projects/members/group_links_spec.rb9
-rw-r--r--spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb2
-rw-r--r--spec/features/projects/members/group_members_spec.rb90
-rw-r--r--spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb6
-rw-r--r--spec/features/projects/members/master_manages_access_requests_spec.rb2
-rw-r--r--spec/features/projects/members/member_leaves_project_spec.rb2
-rw-r--r--spec/features/projects/members/owner_cannot_leave_project_spec.rb4
-rw-r--r--spec/features/projects/members/sorting_spec.rb98
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb2
-rw-r--r--spec/features/projects/new_project_spec.rb19
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb239
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb211
-rw-r--r--spec/features/projects/pipelines_spec.rb262
-rw-r--r--spec/features/projects/project_settings_spec.rb17
-rw-r--r--spec/features/projects/services/mattermost_slash_command_spec.rb48
-rw-r--r--spec/features/projects/services/slack_service_spec.rb26
-rw-r--r--spec/features/projects/settings/merge_requests_settings_spec.rb70
-rw-r--r--spec/features/projects/slack_service/slack_service_spec.rb26
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb12
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb4
-rw-r--r--spec/features/search_spec.rb26
-rw-r--r--spec/features/security/admin_access_spec.rb2
-rw-r--r--spec/features/security/group/internal_access_spec.rb123
-rw-r--r--spec/features/security/group/private_access_spec.rb123
-rw-r--r--spec/features/security/group/public_access_spec.rb123
-rw-r--r--spec/features/security/project/internal_access_spec.rb517
-rw-r--r--spec/features/security/project/private_access_spec.rb500
-rw-r--r--spec/features/security/project/public_access_spec.rb515
-rw-r--r--spec/features/security/project/snippet/internal_access_spec.rb124
-rw-r--r--spec/features/security/project/snippet/private_access_spec.rb86
-rw-r--r--spec/features/security/project/snippet/public_access_spec.rb162
-rw-r--r--spec/features/snippets/create_snippet_spec.rb20
-rw-r--r--spec/features/snippets/explore_spec.rb16
-rw-r--r--spec/features/snippets/search_snippets_spec.rb66
-rw-r--r--spec/features/todos/todos_spec.rb6
-rw-r--r--spec/features/u2f_spec.rb9
-rw-r--r--spec/features/unsubscribe_links_spec.rb14
-rw-r--r--spec/features/users_spec.rb19
-rw-r--r--spec/features/variables_spec.rb25
-rw-r--r--spec/finders/access_requests_finder_spec.rb15
-rw-r--r--spec/finders/group_projects_finder_spec.rb5
-rw-r--r--spec/finders/issues_finder_spec.rb106
-rw-r--r--spec/finders/labels_finder_spec.rb17
-rw-r--r--spec/finders/merge_requests_finder_spec.rb11
-rw-r--r--spec/finders/notes_finder_spec.rb200
-rw-r--r--spec/finders/snippets_finder_spec.rb84
-rw-r--r--spec/fixtures/api/schemas/user/login.json37
-rw-r--r--spec/fixtures/api/schemas/user/public.json79
-rw-r--r--spec/fixtures/emails/outlook_html.eml140
-rw-r--r--spec/helpers/application_helper_spec.rb42
-rw-r--r--spec/helpers/diff_helper_spec.rb61
-rw-r--r--spec/helpers/gitlab_markdown_helper_spec.rb2
-rw-r--r--spec/helpers/issuables_helper_spec.rb21
-rw-r--r--spec/helpers/labels_helper_spec.rb4
-rw-r--r--spec/helpers/members_helper_spec.rb12
-rw-r--r--spec/helpers/milestones_helper_spec.rb19
-rw-r--r--spec/helpers/preferences_helper_spec.rb41
-rw-r--r--spec/helpers/search_helper_spec.rb32
-rw-r--r--spec/helpers/sidekiq_helper_spec.rb23
-rw-r--r--spec/javascripts/.eslintrc15
-rw-r--r--spec/javascripts/abuse_reports_spec.js.es638
-rw-r--r--spec/javascripts/activities_spec.js.es65
-rw-r--r--spec/javascripts/application_spec.js37
-rw-r--r--spec/javascripts/awards_handler_spec.js13
-rw-r--r--spec/javascripts/behaviors/autosize_spec.js2
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js2
-rw-r--r--spec/javascripts/behaviors/requires_input_spec.js2
-rw-r--r--spec/javascripts/boards/boards_store_spec.js.es6224
-rw-r--r--spec/javascripts/boards/issue_spec.js.es65
-rw-r--r--spec/javascripts/boards/list_spec.js.es613
-rw-r--r--spec/javascripts/boards/mock_data.js.es67
-rw-r--r--spec/javascripts/bootstrap_linked_tabs_spec.js.es659
-rw-r--r--spec/javascripts/build_spec.js.es6184
-rw-r--r--spec/javascripts/dashboard_spec.js.es63
-rw-r--r--spec/javascripts/datetime_utility_spec.js.es62
-rw-r--r--spec/javascripts/diff_comments_store_spec.js.es65
-rw-r--r--spec/javascripts/environments/environment_actions_spec.js.es667
-rw-r--r--spec/javascripts/environments/environment_external_url_spec.js.es622
-rw-r--r--spec/javascripts/environments/environment_item_spec.js.es6229
-rw-r--r--spec/javascripts/environments/environment_rollback_spec.js.es648
-rw-r--r--spec/javascripts/environments/environment_stop_spec.js.es628
-rw-r--r--spec/javascripts/environments/environments_store_spec.js.es671
-rw-r--r--spec/javascripts/environments/mock_data.js.es6135
-rw-r--r--spec/javascripts/extensions/array_spec.js23
-rw-r--r--spec/javascripts/extensions/array_spec.js.es646
-rw-r--r--spec/javascripts/extensions/element_spec.js.es638
-rw-r--r--spec/javascripts/extensions/jquery_spec.js2
-rw-r--r--spec/javascripts/extensions/object_spec.js.es625
-rw-r--r--spec/javascripts/fixtures/abuse_reports.html.haml16
-rw-r--r--spec/javascripts/fixtures/abuse_reports.rb27
-rw-r--r--spec/javascripts/fixtures/application.html.haml2
-rw-r--r--spec/javascripts/fixtures/awards_handler.html.haml52
-rw-r--r--spec/javascripts/fixtures/builds.rb33
-rw-r--r--spec/javascripts/fixtures/comments.html.haml21
-rw-r--r--spec/javascripts/fixtures/emoji_menu.js2
-rw-r--r--spec/javascripts/fixtures/environments/element.html.haml1
-rw-r--r--spec/javascripts/fixtures/environments/environments.html.haml9
-rw-r--r--spec/javascripts/fixtures/environments/table.html.haml11
-rw-r--r--spec/javascripts/fixtures/event_filter.html.haml4
-rw-r--r--spec/javascripts/fixtures/issuable.html.haml2
-rw-r--r--spec/javascripts/fixtures/issue_note.html.haml12
-rw-r--r--spec/javascripts/fixtures/issues.rb10
-rw-r--r--spec/javascripts/fixtures/linked_tabs.html.haml13
-rw-r--r--spec/javascripts/fixtures/pipeline_graph.html.haml15
-rw-r--r--spec/javascripts/fixtures/right_sidebar.html.haml17
-rw-r--r--spec/javascripts/fixtures/signin_tabs.html.haml5
-rw-r--r--spec/javascripts/fixtures/zen_mode.html.haml8
-rw-r--r--spec/javascripts/gl_dropdown_spec.js.es64
-rw-r--r--spec/javascripts/gl_field_errors_spec.js.es63
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_graph_spec.js6
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_util_spec.js4
-rw-r--r--spec/javascripts/graphs/stat_graph_spec.js4
-rw-r--r--spec/javascripts/header_spec.js2
-rw-r--r--spec/javascripts/issue_spec.js7
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js.es65
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js.es632
-rw-r--r--spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es643
-rw-r--r--spec/javascripts/line_highlighter_spec.js3
-rw-r--r--spec/javascripts/merge_request_spec.js3
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js120
-rw-r--r--spec/javascripts/merge_request_widget_spec.js72
-rw-r--r--spec/javascripts/new_branch_spec.js3
-rw-r--r--spec/javascripts/notes_spec.js25
-rw-r--r--spec/javascripts/pipelines_spec.js.es625
-rw-r--r--spec/javascripts/pretty_time_spec.js.es6134
-rw-r--r--spec/javascripts/project_title_spec.js20
-rw-r--r--spec/javascripts/right_sidebar_spec.js17
-rw-r--r--spec/javascripts/search_autocomplete_spec.js2
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js9
-rw-r--r--spec/javascripts/signin_tabs_memoizer_spec.js.es653
-rw-r--r--spec/javascripts/smart_interval_spec.js.es6180
-rw-r--r--spec/javascripts/spec_helper.js7
-rw-r--r--spec/javascripts/subbable_resource_spec.js.es666
-rw-r--r--spec/javascripts/syntax_highlight_spec.js2
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js4
-rw-r--r--spec/javascripts/u2f/mock_u2f_device.js2
-rw-r--r--spec/javascripts/u2f/register_spec.js4
-rw-r--r--spec/javascripts/vue_common_components/commit_spec.js.es6131
-rw-r--r--spec/javascripts/zen_mode_spec.js14
-rw-r--r--spec/lib/banzai/filter/abstract_link_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/autolink_filter_spec.rb22
-rw-r--r--spec/lib/banzai/filter/commit_range_reference_filter_spec.rb99
-rw-r--r--spec/lib/banzai/filter/commit_reference_filter_spec.rb78
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb108
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb291
-rw-r--r--spec/lib/banzai/filter/math_filter_spec.rb120
-rw-r--r--spec/lib/banzai/filter/merge_request_reference_filter_spec.rb92
-rw-r--r--spec/lib/banzai/filter/milestone_reference_filter_spec.rb108
-rw-r--r--spec/lib/banzai/filter/redactor_filter_spec.rb42
-rw-r--r--spec/lib/banzai/filter/relative_link_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/snippet_reference_filter_spec.rb85
-rw-r--r--spec/lib/banzai/filter/table_of_contents_filter_spec.rb21
-rw-r--r--spec/lib/banzai/reference_parser/base_parser_spec.rb35
-rw-r--r--spec/lib/banzai/reference_parser/commit_parser_spec.rb8
-rw-r--r--spec/lib/banzai/reference_parser/commit_range_parser_spec.rb8
-rw-r--r--spec/lib/banzai/reference_parser/external_issue_parser_spec.rb8
-rw-r--r--spec/lib/banzai/reference_parser/issue_parser_spec.rb10
-rw-r--r--spec/lib/banzai/reference_parser/label_parser_spec.rb8
-rw-r--r--spec/lib/banzai/reference_parser/merge_request_parser_spec.rb13
-rw-r--r--spec/lib/banzai/reference_parser/milestone_parser_spec.rb8
-rw-r--r--spec/lib/banzai/reference_parser/snippet_parser_spec.rb8
-rw-r--r--spec/lib/banzai/reference_parser/user_parser_spec.rb2
-rw-r--r--spec/lib/bitbucket/collection_spec.rb24
-rw-r--r--spec/lib/bitbucket/connection_spec.rb35
-rw-r--r--spec/lib/bitbucket/page_spec.rb50
-rw-r--r--spec/lib/bitbucket/paginator_spec.rb21
-rw-r--r--spec/lib/bitbucket/representation/comment_spec.rb22
-rw-r--r--spec/lib/bitbucket/representation/issue_spec.rb47
-rw-r--r--spec/lib/bitbucket/representation/pull_request_comment_spec.rb34
-rw-r--r--spec/lib/bitbucket/representation/pull_request_spec.rb47
-rw-r--r--spec/lib/bitbucket/representation/user_spec.rb11
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb4
-rw-r--r--spec/lib/constraints/group_url_constrainer_spec.rb28
-rw-r--r--spec/lib/constraints/namespace_url_constrainer_spec.rb35
-rw-r--r--spec/lib/constraints/project_url_constrainer_spec.rb32
-rw-r--r--spec/lib/constraints/user_url_constrainer_spec.rb21
-rw-r--r--spec/lib/event_filter_spec.rb15
-rw-r--r--spec/lib/gitlab/allowable_spec.rb27
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb4
-rw-r--r--spec/lib/gitlab/auth_spec.rb68
-rw-r--r--spec/lib/gitlab/backup/manager_spec.rb127
-rw-r--r--spec/lib/gitlab/badge/build/status_spec.rb4
-rw-r--r--spec/lib/gitlab/bitbucket_import/client_spec.rb67
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb54
-rw-r--r--spec/lib/gitlab/bitbucket_import/project_creator_spec.rb18
-rw-r--r--spec/lib/gitlab/chat_commands/command_spec.rb94
-rw-r--r--spec/lib/gitlab/chat_commands/deploy_spec.rb79
-rw-r--r--spec/lib/gitlab/chat_commands/issue_create_spec.rb68
-rw-r--r--spec/lib/gitlab/chat_commands/issue_search_spec.rb46
-rw-r--r--spec/lib/gitlab/chat_commands/issue_show_spec.rb48
-rw-r--r--spec/lib/gitlab/chat_name_token_spec.rb37
-rw-r--r--spec/lib/gitlab/ci/build/credentials/factory_spec.rb38
-rw-r--r--spec/lib/gitlab/ci/build/credentials/registry_spec.rb41
-rw-r--r--spec/lib/gitlab/ci/config/entry/artifacts_spec.rb45
-rw-r--r--spec/lib/gitlab/ci/config/entry/attributable_spec.rb43
-rw-r--r--spec/lib/gitlab/ci/config/entry/boolean_spec.rb34
-rw-r--r--spec/lib/gitlab/ci/config/entry/cache_spec.rb60
-rw-r--r--spec/lib/gitlab/ci/config/entry/commands_spec.rb49
-rw-r--r--spec/lib/gitlab/ci/config/entry/configurable_spec.rb67
-rw-r--r--spec/lib/gitlab/ci/config/entry/environment_spec.rb217
-rw-r--r--spec/lib/gitlab/ci/config/entry/factory_spec.rb87
-rw-r--r--spec/lib/gitlab/ci/config/entry/global_spec.rb314
-rw-r--r--spec/lib/gitlab/ci/config/entry/hidden_spec.rb47
-rw-r--r--spec/lib/gitlab/ci/config/entry/image_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb148
-rw-r--r--spec/lib/gitlab/ci/config/entry/jobs_spec.rb89
-rw-r--r--spec/lib/gitlab/ci/config/entry/key_spec.rb34
-rw-r--r--spec/lib/gitlab/ci/config/entry/paths_spec.rb34
-rw-r--r--spec/lib/gitlab/ci/config/entry/script_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/config/entry/services_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/config/entry/stage_spec.rb38
-rw-r--r--spec/lib/gitlab/ci/config/entry/stages_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/config/entry/trigger_spec.rb56
-rw-r--r--spec/lib/gitlab/ci/config/entry/undefined_spec.rb41
-rw-r--r--spec/lib/gitlab/ci/config/entry/unspecified_spec.rb32
-rw-r--r--spec/lib/gitlab/ci/config/entry/validatable_spec.rb54
-rw-r--r--spec/lib/gitlab/ci/config/entry/validator_spec.rb55
-rw-r--r--spec/lib/gitlab/ci/config/entry/variables_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/config/node/artifacts_spec.rb45
-rw-r--r--spec/lib/gitlab/ci/config/node/attributable_spec.rb43
-rw-r--r--spec/lib/gitlab/ci/config/node/boolean_spec.rb34
-rw-r--r--spec/lib/gitlab/ci/config/node/cache_spec.rb60
-rw-r--r--spec/lib/gitlab/ci/config/node/commands_spec.rb49
-rw-r--r--spec/lib/gitlab/ci/config/node/configurable_spec.rb67
-rw-r--r--spec/lib/gitlab/ci/config/node/environment_spec.rb217
-rw-r--r--spec/lib/gitlab/ci/config/node/factory_spec.rb87
-rw-r--r--spec/lib/gitlab/ci/config/node/global_spec.rb299
-rw-r--r--spec/lib/gitlab/ci/config/node/hidden_spec.rb47
-rw-r--r--spec/lib/gitlab/ci/config/node/image_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/config/node/job_spec.rb140
-rw-r--r--spec/lib/gitlab/ci/config/node/jobs_spec.rb89
-rw-r--r--spec/lib/gitlab/ci/config/node/key_spec.rb34
-rw-r--r--spec/lib/gitlab/ci/config/node/paths_spec.rb34
-rw-r--r--spec/lib/gitlab/ci/config/node/script_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/config/node/services_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/config/node/stage_spec.rb38
-rw-r--r--spec/lib/gitlab/ci/config/node/stages_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/config/node/trigger_spec.rb56
-rw-r--r--spec/lib/gitlab/ci/config/node/undefined_spec.rb41
-rw-r--r--spec/lib/gitlab/ci/config/node/unspecified_spec.rb32
-rw-r--r--spec/lib/gitlab/ci/config/node/validatable_spec.rb54
-rw-r--r--spec/lib/gitlab/ci/config/node/validator_spec.rb55
-rw-r--r--spec/lib/gitlab/ci/config/node/variables_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/status/build/cancelable_spec.rb86
-rw-r--r--spec/lib/gitlab/ci/status/build/common_spec.rb37
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb141
-rw-r--r--spec/lib/gitlab/ci/status/build/play_spec.rb82
-rw-r--r--spec/lib/gitlab/ci/status/build/retryable_spec.rb86
-rw-r--r--spec/lib/gitlab/ci/status/build/stop_spec.rb84
-rw-r--r--spec/lib/gitlab/ci/status/canceled_spec.rb19
-rw-r--r--spec/lib/gitlab/ci/status/created_spec.rb19
-rw-r--r--spec/lib/gitlab/ci/status/extended_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/status/factory_spec.rb24
-rw-r--r--spec/lib/gitlab/ci/status/failed_spec.rb19
-rw-r--r--spec/lib/gitlab/ci/status/pending_spec.rb19
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/common_spec.rb36
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/factory_spec.rb59
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb65
-rw-r--r--spec/lib/gitlab/ci/status/running_spec.rb19
-rw-r--r--spec/lib/gitlab/ci/status/skipped_spec.rb19
-rw-r--r--spec/lib/gitlab/ci/status/stage/common_spec.rb43
-rw-r--r--spec/lib/gitlab/ci/status/stage/factory_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/status/success_spec.rb19
-rw-r--r--spec/lib/gitlab/closing_issue_extractor_spec.rb3
-rw-r--r--spec/lib/gitlab/contributions_calendar_spec.rb104
-rw-r--r--spec/lib/gitlab/cycle_analytics/code_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/events_spec.rb326
-rw-r--r--spec/lib/gitlab/cycle_analytics/issue_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/permissions_spec.rb127
-rw-r--r--spec/lib/gitlab/cycle_analytics/plan_event_spec.rb18
-rw-r--r--spec/lib/gitlab/cycle_analytics/production_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/review_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/shared_event_spec.rb21
-rw-r--r--spec/lib/gitlab/cycle_analytics/staging_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/test_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/updater_spec.rb25
-rw-r--r--spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb21
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb24
-rw-r--r--spec/lib/gitlab/email/reply_parser_spec.rb4
-rw-r--r--spec/lib/gitlab/file_detector_spec.rb59
-rw-r--r--spec/lib/gitlab/gfm/reference_rewriter_spec.rb8
-rw-r--r--spec/lib/gitlab/git_access_spec.rb25
-rw-r--r--spec/lib/gitlab/git_access_wiki_spec.rb27
-rw-r--r--spec/lib/gitlab/github_import/branch_formatter_spec.rb12
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb5
-rw-r--r--spec/lib/gitlab/github_import/issue_formatter_spec.rb10
-rw-r--r--spec/lib/gitlab/identifier_spec.rb5
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml11
-rw-r--r--spec/lib/gitlab/import_export/avatar_restorer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/members_mapper_spec.rb11
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml4
-rw-r--r--spec/lib/gitlab/ldap/config_spec.rb120
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb2
-rw-r--r--spec/lib/gitlab/middleware/multipart_spec.rb74
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb23
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb89
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb3
-rw-r--r--spec/lib/gitlab/regex_spec.rb16
-rw-r--r--spec/lib/gitlab/routing_spec.rb23
-rw-r--r--spec/lib/gitlab/search_results_spec.rb67
-rw-r--r--spec/lib/gitlab/sidekiq_throttler_spec.rb28
-rw-r--r--spec/lib/gitlab/sql/union_spec.rb22
-rw-r--r--spec/lib/light_url_builder_spec.rb119
-rw-r--r--spec/lib/mattermost/session_spec.rb99
-rw-r--r--spec/mailers/emails/profile_spec.rb2
-rw-r--r--spec/mailers/notify_spec.rb128
-rw-r--r--spec/migrations/migrate_process_commit_worker_jobs_spec.rb217
-rw-r--r--spec/models/broadcast_message_spec.rb2
-rw-r--r--spec/models/build_spec.rb289
-rw-r--r--spec/models/chat_name_spec.rb16
-rw-r--r--spec/models/ci/build_spec.rb6
-rw-r--r--spec/models/ci/pipeline_spec.rb421
-rw-r--r--spec/models/ci/stage_spec.rb135
-rw-r--r--spec/models/ci/variable_spec.rb7
-rw-r--r--spec/models/commit_range_spec.rb23
-rw-r--r--spec/models/commit_spec.rb71
-rw-r--r--spec/models/commit_status_spec.rb54
-rw-r--r--spec/models/concerns/access_requestable_spec.rb8
-rw-r--r--spec/models/concerns/has_status_spec.rb98
-rw-r--r--spec/models/concerns/issuable_spec.rb29
-rw-r--r--spec/models/concerns/milestoneish_spec.rb20
-rw-r--r--spec/models/concerns/routable_spec.rb71
-rw-r--r--spec/models/concerns/subscribable_spec.rb117
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb7
-rw-r--r--spec/models/cycle_analytics/code_spec.rb2
-rw-r--r--spec/models/cycle_analytics/issue_spec.rb2
-rw-r--r--spec/models/cycle_analytics/plan_spec.rb2
-rw-r--r--spec/models/cycle_analytics/production_spec.rb2
-rw-r--r--spec/models/cycle_analytics/review_spec.rb2
-rw-r--r--spec/models/cycle_analytics/staging_spec.rb2
-rw-r--r--spec/models/cycle_analytics/summary_spec.rb2
-rw-r--r--spec/models/cycle_analytics/test_spec.rb2
-rw-r--r--spec/models/discussion_spec.rb28
-rw-r--r--spec/models/environment_spec.rb83
-rw-r--r--spec/models/event_spec.rb50
-rw-r--r--spec/models/generic_commit_status_spec.rb16
-rw-r--r--spec/models/group_label_spec.rb12
-rw-r--r--spec/models/group_spec.rb12
-rw-r--r--spec/models/guest_spec.rb47
-rw-r--r--spec/models/issue_spec.rb126
-rw-r--r--spec/models/key_spec.rb32
-rw-r--r--spec/models/member_spec.rb26
-rw-r--r--spec/models/merge_request_diff_spec.rb34
-rw-r--r--spec/models/merge_request_spec.rb221
-rw-r--r--spec/models/milestone_spec.rb68
-rw-r--r--spec/models/namespace_spec.rb47
-rw-r--r--spec/models/note_spec.rb40
-rw-r--r--spec/models/project_group_link_spec.rb16
-rw-r--r--spec/models/project_label_spec.rb4
-rw-r--r--spec/models/project_services/chat_message/build_message_spec.rb57
-rw-r--r--spec/models/project_services/chat_message/issue_message_spec.rb67
-rw-r--r--spec/models/project_services/chat_message/merge_message_spec.rb51
-rw-r--r--spec/models/project_services/chat_message/note_message_spec.rb130
-rw-r--r--spec/models/project_services/chat_message/pipeline_message_spec.rb67
-rw-r--r--spec/models/project_services/chat_message/push_message_spec.rb88
-rw-r--r--spec/models/project_services/chat_message/wiki_page_message_spec.rb73
-rw-r--r--spec/models/project_services/chat_notification_service_spec.rb11
-rw-r--r--spec/models/project_services/chat_service_spec.rb15
-rw-r--r--spec/models/project_services/gitlab_issue_tracker_service_spec.rb6
-rw-r--r--spec/models/project_services/jira_service_spec.rb91
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb159
-rw-r--r--spec/models/project_services/mattermost_notification_service_spec.rb5
-rw-r--r--spec/models/project_services/mattermost_slash_commands_service_spec.rb99
-rw-r--r--spec/models/project_services/pipeline_email_service_spec.rb17
-rw-r--r--spec/models/project_services/slack_notification_service_spec.rb5
-rw-r--r--spec/models/project_services/slack_service/build_message_spec.rb57
-rw-r--r--spec/models/project_services/slack_service/issue_message_spec.rb67
-rw-r--r--spec/models/project_services/slack_service/merge_message_spec.rb51
-rw-r--r--spec/models/project_services/slack_service/note_message_spec.rb130
-rw-r--r--spec/models/project_services/slack_service/pipeline_message_spec.rb55
-rw-r--r--spec/models/project_services/slack_service/push_message_spec.rb88
-rw-r--r--spec/models/project_services/slack_service/wiki_page_message_spec.rb73
-rw-r--r--spec/models/project_services/slack_service_spec.rb327
-rw-r--r--spec/models/project_spec.rb314
-rw-r--r--spec/models/project_team_spec.rb107
-rw-r--r--spec/models/repository_spec.rb634
-rw-r--r--spec/models/route_spec.rb29
-rw-r--r--spec/models/service_spec.rb13
-rw-r--r--spec/models/snippet_spec.rb51
-rw-r--r--spec/models/subscription_spec.rb20
-rw-r--r--spec/models/user_spec.rb171
-rw-r--r--spec/policies/group_policy_spec.rb108
-rw-r--r--spec/policies/project_policy_spec.rb38
-rw-r--r--spec/rake_helper.rb2
-rw-r--r--spec/requests/api/access_requests_spec.rb22
-rw-r--r--spec/requests/api/api_helpers_spec.rb293
-rw-r--r--spec/requests/api/api_internal_helpers_spec.rb32
-rw-r--r--spec/requests/api/award_emoji_spec.rb2
-rw-r--r--spec/requests/api/boards_spec.rb2
-rw-r--r--spec/requests/api/branches_spec.rb62
-rw-r--r--spec/requests/api/builds_spec.rb4
-rw-r--r--spec/requests/api/commits_spec.rb81
-rw-r--r--spec/requests/api/deploy_keys_spec.rb6
-rw-r--r--spec/requests/api/deployments_spec.rb2
-rw-r--r--spec/requests/api/doorkeeper_access_spec.rb2
-rw-r--r--spec/requests/api/environments_spec.rb19
-rw-r--r--spec/requests/api/files_spec.rb2
-rw-r--r--spec/requests/api/groups_spec.rb85
-rw-r--r--spec/requests/api/helpers_spec.rb446
-rw-r--r--spec/requests/api/internal_spec.rb42
-rw-r--r--spec/requests/api/issues_spec.rb91
-rw-r--r--spec/requests/api/keys_spec.rb2
-rw-r--r--spec/requests/api/labels_spec.rb53
-rw-r--r--spec/requests/api/members_spec.rb20
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb2
-rw-r--r--spec/requests/api/merge_requests_spec.rb77
-rw-r--r--spec/requests/api/milestones_spec.rb25
-rw-r--r--spec/requests/api/namespaces_spec.rb2
-rw-r--r--spec/requests/api/notes_spec.rb4
-rw-r--r--spec/requests/api/notification_settings_spec.rb2
-rw-r--r--spec/requests/api/pipelines_spec.rb60
-rw-r--r--spec/requests/api/project_hooks_spec.rb2
-rw-r--r--spec/requests/api/project_snippets_spec.rb66
-rw-r--r--spec/requests/api/projects_spec.rb483
-rw-r--r--spec/requests/api/repositories_spec.rb36
-rw-r--r--spec/requests/api/runners_spec.rb4
-rw-r--r--spec/requests/api/services_spec.rb63
-rw-r--r--spec/requests/api/session_spec.rb18
-rw-r--r--spec/requests/api/settings_spec.rb2
-rw-r--r--spec/requests/api/snippets_spec.rb157
-rw-r--r--spec/requests/api/system_hooks_spec.rb2
-rw-r--r--spec/requests/api/tags_spec.rb57
-rw-r--r--spec/requests/api/triggers_spec.rb11
-rw-r--r--spec/requests/api/users_spec.rb181
-rw-r--r--spec/requests/api/variables_spec.rb2
-rw-r--r--spec/requests/api/version_spec.rb2
-rw-r--r--spec/requests/ci/api/builds_spec.rb127
-rw-r--r--spec/requests/ci/api/runners_spec.rb2
-rw-r--r--spec/requests/ci/api/triggers_spec.rb2
-rw-r--r--spec/requests/git_http_spec.rb34
-rw-r--r--spec/requests/jwt_controller_spec.rb18
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb140
-rw-r--r--spec/routing/admin_routing_spec.rb14
-rw-r--r--spec/routing/project_routing_spec.rb1045
-rw-r--r--spec/routing/routing_spec.rb32
-rw-r--r--spec/serializers/analytics_build_entity_spec.rb94
-rw-r--r--spec/serializers/analytics_build_serializer_spec.rb18
-rw-r--r--spec/serializers/analytics_generic_entity_spec.rb39
-rw-r--r--spec/serializers/analytics_issue_serializer_spec.rb29
-rw-r--r--spec/serializers/analytics_merge_request_serializer_spec.rb30
-rw-r--r--spec/serializers/build_entity_spec.rb29
-rw-r--r--spec/serializers/commit_entity_spec.rb6
-rw-r--r--spec/serializers/deployment_entity_spec.rb2
-rw-r--r--spec/serializers/entity_date_helper_spec.rb45
-rw-r--r--spec/serializers/environment_entity_spec.rb2
-rw-r--r--spec/serializers/environment_serializer_spec.rb6
-rw-r--r--spec/services/access_token_validation_service_spec.rb41
-rw-r--r--spec/services/after_branch_delete_service_spec.rb15
-rw-r--r--spec/services/chat_names/authorize_user_service_spec.rb25
-rw-r--r--spec/services/chat_names/find_user_service_spec.rb36
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb17
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb137
-rw-r--r--spec/services/ci/send_pipeline_notification_service_spec.rb48
-rw-r--r--spec/services/ci/stop_environments_service_spec.rb105
-rw-r--r--spec/services/delete_branch_service_spec.rb41
-rw-r--r--spec/services/delete_merged_branches_service_spec.rb54
-rw-r--r--spec/services/destroy_group_service_spec.rb40
-rw-r--r--spec/services/discussions/resolve_service_spec.rb52
-rw-r--r--spec/services/git_push_service_spec.rb122
-rw-r--r--spec/services/git_tag_push_service_spec.rb8
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb6
-rw-r--r--spec/services/issues/build_service_spec.rb130
-rw-r--r--spec/services/issues/close_service_spec.rb2
-rw-r--r--spec/services/issues/create_service_spec.rb43
-rw-r--r--spec/services/issues/move_service_spec.rb10
-rw-r--r--spec/services/issues/update_service_spec.rb37
-rw-r--r--spec/services/labels/transfer_service_spec.rb2
-rw-r--r--spec/services/members/approve_access_request_service_spec.rb4
-rw-r--r--spec/services/members/destroy_service_spec.rb1
-rw-r--r--spec/services/members/request_access_service_spec.rb31
-rw-r--r--spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb45
-rw-r--r--spec/services/merge_requests/build_service_spec.rb12
-rw-r--r--spec/services/merge_requests/close_service_spec.rb2
-rw-r--r--spec/services/merge_requests/get_urls_service_spec.rb6
-rw-r--r--spec/services/merge_requests/merge_request_diff_cache_service_spec.rb1
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb22
-rw-r--r--spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb163
-rw-r--r--spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb179
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb44
-rw-r--r--spec/services/merge_requests/reopen_service_spec.rb2
-rw-r--r--spec/services/merge_requests/update_service_spec.rb33
-rw-r--r--spec/services/notes/create_service_spec.rb37
-rw-r--r--spec/services/notification_service_spec.rb246
-rw-r--r--spec/services/projects/create_service_spec.rb207
-rw-r--r--spec/services/projects/destroy_service_spec.rb30
-rw-r--r--spec/services/slash_commands/interpret_service_spec.rb4
-rw-r--r--spec/services/system_note_service_spec.rb250
-rw-r--r--spec/spec_helper.rb12
-rw-r--r--spec/support/capybara.rb10
-rw-r--r--spec/support/carrierwave.rb2
-rw-r--r--spec/support/cycle_analytics_helpers.rb3
-rw-r--r--spec/support/database_connection_helpers.rb9
-rw-r--r--spec/support/db_cleaner.rb4
-rw-r--r--spec/support/email_helpers.rb28
-rw-r--r--spec/support/features/issuable_slash_commands_shared_examples.rb10
-rw-r--r--spec/support/javascript_fixtures_helpers.rb7
-rw-r--r--spec/support/jira_service_helper.rb8
-rw-r--r--spec/support/login_helpers.rb3
-rw-r--r--spec/support/matchers/access_matchers.rb36
-rw-r--r--spec/support/matchers/be_url.rb5
-rw-r--r--spec/support/matchers/have_issuable_counts.rb8
-rw-r--r--spec/support/matchers/is_within.rb9
-rw-r--r--spec/support/matchers/markdown_matchers.rb6
-rw-r--r--spec/support/notify_shared_examples.rb2
-rw-r--r--spec/support/project_features_apply_to_issuables_shared_examples.rb56
-rw-r--r--spec/support/rake_helpers.rb4
-rw-r--r--spec/support/reference_parser_shared_examples.rb43
-rw-r--r--spec/support/search_helpers.rb5
-rw-r--r--spec/support/services/issuable_update_service_shared_examples.rb17
-rw-r--r--spec/support/services_shared_context.rb6
-rw-r--r--spec/support/setup_builds_storage.rb17
-rw-r--r--spec/support/slack_mattermost_shared_examples.rb328
-rw-r--r--spec/support/test_env.rb2
-rw-r--r--spec/support/upload_helpers.rb16
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb33
-rw-r--r--spec/tasks/gitlab/check_rake_spec.rb51
-rw-r--r--spec/tasks/gitlab/mail_google_schema_whitelisting.rb2
-rw-r--r--spec/tasks/gitlab/task_helpers_spec.rb96
-rw-r--r--spec/tasks/gitlab/users_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/workhorse_rake_spec.rb81
-rw-r--r--spec/uploaders/attachment_uploader_spec.rb18
-rw-r--r--spec/uploaders/avatar_uploader_spec.rb18
-rw-r--r--spec/uploaders/file_uploader_spec.rb12
-rw-r--r--spec/views/layouts/_head.html.haml_spec.rb4
-rw-r--r--spec/views/projects/builds/_build.html.haml_spec.rb28
-rw-r--r--spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb28
-rw-r--r--spec/views/projects/builds/show.html.haml_spec.rb165
-rw-r--r--spec/views/projects/commit/_commit_box.html.haml_spec.rb2
-rw-r--r--spec/views/projects/edit.html.haml_spec.rb24
-rw-r--r--spec/views/projects/merge_requests/_new_submit.html.haml_spec.rb31
-rw-r--r--spec/views/projects/pipelines/show.html.haml_spec.rb2
-rw-r--r--spec/workers/authorized_projects_worker_spec.rb33
-rw-r--r--spec/workers/build_email_worker_spec.rb3
-rw-r--r--spec/workers/delete_merged_branches_worker_spec.rb19
-rw-r--r--spec/workers/emails_on_push_worker_spec.rb5
-rw-r--r--spec/workers/new_note_worker_spec.rb49
-rw-r--r--spec/workers/pipeline_metrics_worker_spec.rb18
-rw-r--r--spec/workers/pipeline_notification_worker_spec.rb133
-rw-r--r--spec/workers/pipeline_success_worker_spec.rb2
-rw-r--r--spec/workers/process_commit_worker_spec.rb29
-rw-r--r--spec/workers/project_cache_worker_spec.rb86
-rw-r--r--vendor/assets/fonts/KaTeX_AMS-Regular.eotbin0 -> 71656 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_AMS-Regular.ttfbin0 -> 71428 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_AMS-Regular.woffbin0 -> 40200 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_AMS-Regular.woff2bin0 -> 33188 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Caligraphic-Bold.eotbin0 -> 19836 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttfbin0 -> 19588 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Caligraphic-Bold.woffbin0 -> 12136 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2bin0 -> 10604 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Caligraphic-Regular.eotbin0 -> 19220 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttfbin0 -> 18960 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Caligraphic-Regular.woffbin0 -> 11868 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2bin0 -> 10396 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Fraktur-Bold.eotbin0 -> 36200 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Fraktur-Bold.ttfbin0 -> 35968 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Fraktur-Bold.woffbin0 -> 23388 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2bin0 -> 20476 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Fraktur-Regular.eotbin0 -> 34896 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Fraktur-Regular.ttfbin0 -> 34652 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Fraktur-Regular.woffbin0 -> 22844 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2bin0 -> 19868 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Bold.eotbin0 -> 60688 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Bold.ttfbin0 -> 60468 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Bold.woffbin0 -> 35480 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Bold.woff2bin0 -> 29492 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Italic.eotbin0 -> 44132 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Italic.ttfbin0 -> 43904 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Italic.woffbin0 -> 24880 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Italic.woff2bin0 -> 21032 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Regular.eotbin0 -> 68228 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Regular.ttfbin0 -> 67996 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Regular.woffbin0 -> 37620 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Main-Regular.woff2bin0 -> 31220 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-BoldItalic.eotbin0 -> 39990 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-BoldItalic.ttfbin0 -> 39744 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-BoldItalic.woffbin0 -> 23192 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2bin0 -> 20036 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-Italic.eotbin0 -> 41676 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-Italic.ttfbin0 -> 41448 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-Italic.woffbin0 -> 23820 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-Italic.woff2bin0 -> 20432 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-Regular.eotbin0 -> 41536 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-Regular.ttfbin0 -> 41304 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-Regular.woffbin0 -> 23712 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Math-Regular.woff2bin0 -> 20344 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Bold.eotbin0 -> 34204 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Bold.ttfbin0 -> 33964 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Bold.woffbin0 -> 19196 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2bin0 -> 16020 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Italic.eotbin0 -> 31320 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Italic.ttfbin0 -> 31072 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Italic.woffbin0 -> 18080 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2bin0 -> 15152 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Regular.eotbin0 -> 30212 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Regular.ttfbin0 -> 29960 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Regular.woffbin0 -> 16744 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2bin0 -> 13908 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Script-Regular.eotbin0 -> 25104 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Script-Regular.ttfbin0 -> 24864 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Script-Regular.woffbin0 -> 13856 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Script-Regular.woff2bin0 -> 12276 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size1-Regular.eotbin0 -> 13408 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size1-Regular.ttfbin0 -> 13172 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size1-Regular.woffbin0 -> 6980 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size1-Regular.woff2bin0 -> 5820 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size2-Regular.eotbin0 -> 12648 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size2-Regular.ttfbin0 -> 12412 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size2-Regular.woffbin0 -> 6684 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size2-Regular.woff2bin0 -> 5560 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size3-Regular.eotbin0 -> 8596 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size3-Regular.ttfbin0 -> 8360 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size3-Regular.woffbin0 -> 4776 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size3-Regular.woff2bin0 -> 3856 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size4-Regular.eotbin0 -> 11520 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size4-Regular.ttfbin0 -> 11284 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size4-Regular.woffbin0 -> 6456 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Size4-Regular.woff2bin0 -> 5172 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Typewriter-Regular.eotbin0 -> 35784 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Typewriter-Regular.ttfbin0 -> 35528 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Typewriter-Regular.woffbin0 -> 20712 bytes
-rw-r--r--vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2bin0 -> 17344 bytes
-rw-r--r--vendor/assets/javascripts/es6-promise.auto.js1159
-rw-r--r--vendor/assets/javascripts/jquery.timeago.js182
-rw-r--r--vendor/assets/javascripts/katex.js8683
-rw-r--r--vendor/assets/javascripts/timeago.js237
-rw-r--r--vendor/assets/javascripts/vue.full.js15894
-rw-r--r--vendor/assets/javascripts/vue.min.js10
-rw-r--r--vendor/assets/stylesheets/katex.css975
2868 files changed, 79204 insertions, 35378 deletions
diff --git a/.eslintignore b/.eslintignore
index d9c2233c9d7..b4bfa5a1f7a 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,4 +1,6 @@
+/coverage/
/coverage-javascript/
+/node_modules/
/public/
/tmp/
/vendor/
diff --git a/.eslintrc b/.eslintrc
index fd26215b843..e13f76b213c 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,29 +1,20 @@
{
- "extends": "airbnb",
+ "env": {
+ "jquery": true,
+ "browser": true,
+ "es6": true
+ },
+ "extends": "airbnb-base",
+ "globals": {
+ "_": false,
+ "gl": false,
+ "gon": false,
+ "localStorage": false
+ },
"plugins": [
"filenames"
],
"rules": {
"filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"]
- },
- "globals": {
- "$": false,
- "_": false,
- "beforeEach": false,
- "d3": false,
- "define": false,
- "describe": false,
- "document": false,
- "expect": false,
- "fixture": false,
- "gl": false,
- "it": false,
- "jQuery": false,
- "Mousetrap": false,
- "spyOn": false,
- "spyOnEvent": false,
- "Turbolinks": false,
- "window": false
}
}
-
diff --git a/.gitattributes b/.gitattributes
index ab791a4cd6c..70cce05d2b5 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1 @@
-CHANGELOG.md merge=union
*.js.es6 gitlab-language=javascript
diff --git a/.gitignore b/.gitignore
index 6a1002621f4..0b602d613c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@
.chef
.directory
/.envrc
+eslint-report.html
/.gitlab_shell_secret
.idea
/.rbenv-version
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 195783454f9..e522d47d19d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,7 +1,7 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3-git-2.7-phantomjs-2.1"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-git-2.7-phantomjs-2.1-node-7.1"
cache:
- key: "ruby-231"
+ key: "ruby-233"
paths:
- vendor/ruby
@@ -20,7 +20,7 @@ before_script:
- source ./scripts/prepare_build.sh
- cp config/gitlab.yml.example config/gitlab.yml
- bundle --version
- - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"'
+ - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) $FLAGS'
- retry gem install knapsack
- '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate add_limits_mysql'
@@ -30,7 +30,12 @@ stages:
- post-test
- pages
-# Prepare and merge knapsack tests
+# Predefined scopes
+.dedicated-runner: &dedicated-runner
+ tags:
+ - gitlab-org
+ - 2gb
+
.knapsack-state: &knapsack-state
services: []
variables:
@@ -45,26 +50,6 @@ stages:
paths:
- knapsack/
-knapsack:
- <<: *knapsack-state
- stage: prepare
- script:
- - mkdir -p knapsack/
- - '[[ -f knapsack/rspec_report.json ]] || echo "{}" > knapsack/rspec_report.json'
- - '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json'
-
-update-knapsack:
- <<: *knapsack-state
- stage: post-test
- script:
- - scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json
- - scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json
- - rm -f knapsack/*_node_*.json
- only:
- - master
-
-# Execute all testing suites
-
.use-db: &use-db
services:
- mysql:latest
@@ -72,9 +57,9 @@ update-knapsack:
.rspec-knapsack: &rspec-knapsack
stage: test
+ <<: *dedicated-runner
<<: *use-db
script:
- - bundle exec rake assets:precompile 2>/dev/null
- JOB_NAME=( $CI_BUILD_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]}
@@ -90,9 +75,9 @@ update-knapsack:
.spinach-knapsack: &spinach-knapsack
stage: test
+ <<: *dedicated-runner
<<: *use-db
script:
- - bundle exec rake assets:precompile 2>/dev/null
- JOB_NAME=( $CI_BUILD_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]}
@@ -106,6 +91,44 @@ update-knapsack:
- knapsack/
- coverage/
+# Prepare and merge knapsack tests
+
+knapsack:
+ <<: *knapsack-state
+ <<: *dedicated-runner
+ stage: prepare
+ script:
+ - mkdir -p knapsack/
+ - '[[ -f knapsack/rspec_report.json ]] || echo "{}" > knapsack/rspec_report.json'
+ - '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json'
+
+setup-test-env:
+ <<: *use-db
+ <<: *dedicated-runner
+ stage: prepare
+ script:
+ - bundle exec rake assets:precompile 2>/dev/null
+ - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
+ artifacts:
+ expire_in: 7d
+ paths:
+ - public/assets
+ - tmp/tests
+
+update-knapsack:
+ <<: *knapsack-state
+ <<: *dedicated-runner
+ stage: post-test
+ script:
+ - scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json
+ - scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json
+ - rm -f knapsack/*_node_*.json
+ only:
+ - master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
+ - master@gitlab/gitlabhq
+ - master@gitlab/gitlab-ee
+
rspec 0 20: *rspec-knapsack
rspec 1 20: *rspec-knapsack
rspec 2 20: *rspec-knapsack
@@ -143,7 +166,10 @@ spinach 9 10: *spinach-knapsack
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.1-git-2.7-phantomjs-2.1"
<<: *use-db
only:
- - master
+ - master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
+ - master@gitlab/gitlabhq
+ - master@gitlab/gitlab-ee
cache:
key: "ruby21"
paths:
@@ -151,10 +177,12 @@ spinach 9 10: *spinach-knapsack
.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
@@ -199,6 +227,7 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21
.exec: &exec
<<: *ruby-static-analysis
+ <<: *dedicated-runner
stage: test
script:
- bundle exec $CI_BUILD_NAME
@@ -213,21 +242,35 @@ rake downtime_check: *exec
rake ee_compat_check:
<<: *exec
only:
- - branches
+ - branches@gitlab-org/gitlab-ce
except:
- master
- tags
+ - /^[\d-]+-stable(-ee)?$/
allow_failure: yes
+ cache:
+ key: "ruby233-ee_compat_check_repo"
+ paths:
+ - ee_compat_check/repo/
+ - vendor/ruby
+ artifacts:
+ name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}_${CI_BUILD_REF}"
+ when: on_failure
+ expire_in: 10d
+ paths:
+ - ee_compat_check/patches/*.patch
rake db:migrate:reset:
stage: test
<<: *use-db
+ <<: *dedicated-runner
script:
- rake db:migrate:reset
rake db:seed_fu:
stage: test
<<: *use-db
+ <<: *dedicated-runner
variables:
SIZE: "1"
SETUP_DB: "false"
@@ -243,12 +286,16 @@ rake db:seed_fu:
- log/development.log
teaspoon:
+ cache:
+ paths:
+ - vendor/ruby
+ - node_modules/
stage: test
<<: *use-db
+ <<: *dedicated-runner
script:
- - curl --silent --location https://deb.nodesource.com/setup_6.x | bash -
- - apt-get install --assume-yes nodejs
- - npm install --global istanbul
+ - npm install
+ - npm link istanbul
- rake teaspoon
artifacts:
name: coverage-javascript
@@ -258,39 +305,48 @@ teaspoon:
lint-doc:
stage: test
+ <<: *dedicated-runner
image: "phusion/baseimage:latest"
before_script: []
script:
- scripts/lint-doc.sh
bundler:check:
- stage: test
- <<: *ruby-static-analysis
- script:
+ stage: test
+ <<: *dedicated-runner
+ <<: *ruby-static-analysis
+ script:
- bundle check
bundler:audit:
stage: test
<<: *ruby-static-analysis
+ <<: *dedicated-runner
only:
- - master
+ - master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
+ - master@gitlab/gitlabhq
+ - master@gitlab/gitlab-ee
script:
- "bundle exec bundle-audit check --update --ignore OSVDB-115941"
migration paths:
stage: test
<<: *use-db
+ <<: *dedicated-runner
variables:
SETUP_DB: "false"
only:
- master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
+ - master@gitlab/gitlabhq
+ - master@gitlab/gitlab-ee
script:
- - git checkout HEAD .
- - git fetch --tags
- - git checkout v8.5.9
+ - git fetch origin v8.5.9
+ - git checkout -f FETCH_HEAD
- 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
+ - bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3
- rake db:drop db:create db:schema:load db:seed_fu
- git checkout $CI_BUILD_REF
- source scripts/prepare_build.sh
@@ -299,6 +355,7 @@ migration paths:
coverage:
stage: post-test
services: []
+ <<: *dedicated-runner
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "true"
@@ -311,19 +368,42 @@ coverage:
- coverage/index.html
- coverage/assets/
-lint-javascript:
+lint:javascript:
+ <<: *dedicated-runner
+ cache:
+ paths:
+ - node_modules/
stage: test
- image: "node:latest"
+ image: "node:7.1"
before_script:
- npm install
script:
- - npm run eslint
+ - npm --silent run eslint
+
+lint:javascript:report:
+ <<: *dedicated-runner
+ cache:
+ paths:
+ - node_modules/
+ stage: post-test
+ image: "node:7.1"
+ before_script:
+ - npm install
+ script:
+ - find app/ spec/ -name '*.js' -or -name '*.js.es6' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files
+ - npm --silent run eslint-report || true # ignore exit code
+ artifacts:
+ name: eslint-report
+ expire_in: 31d
+ paths:
+ - eslint-report.html
# Trigger docs build
# https://gitlab.com/gitlab-com/doc-gitlab-com/blob/master/README.md#deployment-process
trigger_docs:
stage: post-test
image: "alpine"
+ <<: *dedicated-runner
before_script:
- apk update && apk add curl
variables:
@@ -339,11 +419,12 @@ trigger_docs:
notify:slack:
stage: post-test
+ <<: *dedicated-runner
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
script:
- - ./scripts/notify_slack.sh "#builds" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>"
+ - ./scripts/notify_slack.sh "#development" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>"
when: on_failure
only:
- master@gitlab-org/gitlab-ce
@@ -354,23 +435,27 @@ notify:slack:
pages:
before_script: []
stage: pages
+ <<: *dedicated-runner
dependencies:
- coverage
- teaspoon
+ - 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/
artifacts:
paths:
- public
only:
- - master
+ - master@gitlab-org/gitlab-ce
# Insurance in case a gem needed by one of our releases gets yanked from
# rubygems.org in the future.
cache gems:
+ <<: *dedicated-runner
only:
- tags
variables:
@@ -380,3 +465,5 @@ cache gems:
artifacts:
paths:
- vendor/cache
+ only:
+ - master@gitlab-org/gitlab-ce
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
index ac38f0c9521..6d7d88c6791 100644
--- a/.gitlab/issue_templates/Bug.md
+++ b/.gitlab/issue_templates/Bug.md
@@ -21,6 +21,8 @@ logs, and code as it's very hard to read otherwise.)
### Output of checks
+(If you are reporting a bug on GitLab.com, write: This bug happens on GitLab.com)
+
#### Results of GitLab application Check
(For installations with omnibus-gitlab package run and paste the output of:
diff --git a/.rubocop.yml b/.rubocop.yml
index 13df3f99613..80eb4a5c19e 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -292,7 +292,8 @@ Style/MultilineMethodDefinitionBraceLayout:
# Checks indentation of binary operations that span more than one line.
Style/MultilineOperationIndentation:
- Enabled: false
+ Enabled: true
+ EnforcedStyle: indented
# Avoid multi-line `? :` (the ternary operator), use if/unless instead.
Style/MultilineTernaryOperator:
diff --git a/.ruby-version b/.ruby-version
index 2bf1c1ccf36..0bee604df76 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.3.1
+2.3.3
diff --git a/.scss-lint.yml b/.scss-lint.yml
index aae8d9b6dbe..83c68309fa8 100644
--- a/.scss-lint.yml
+++ b/.scss-lint.yml
@@ -30,7 +30,7 @@ linters:
# variable declarations. They should be referred to via variables everywhere
# else.
ColorVariable:
- enabled: false
+ enabled: true
# Which form of comments to prefer in CSS.
Comment:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5b072ce9f60..fb13db4dd1c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,195 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 8.14.4 (2016-12-08)
+
+- Fix diff view permalink highlighting. !7090
+- Fix pipeline author for Slack and use pipeline id for pipeline link. !7506
+- Fix compatibility with Internet Explorer 11 for merge requests. !7525 (Steffen Rauh)
+- Reenables /user API request to return private-token if user is admin and request is made with sudo. !7615
+- Fix Cicking on tabs on pipeline page should set URL. !7709
+- Authorize users into imported GitLab project.
+- Destroy a user's session when they delete their own account.
+- Don't accidentally mark unsafe diff lines as HTML safe.
+- Replace MR access checks with use of MergeRequestsFinder.
+- Remove visible content caching.
+
+## 8.14.3 (2016-12-02)
+
+- Pass commit data to ProcessCommitWorker to reduce Git overhead. !7744
+- Speed up issuable dashboards.
+- Don't change relative URLs to absolute URLs in the Help page.
+- Fixes "ActionView::Template::Error: undefined method `text?` for nil:NilClass" on MR pages.
+- Fix branch validation for GitHub PR where repo/fork was renamed/deleted.
+- Validate state param when filtering issuables.
+
+## 8.14.2 (2016-12-01)
+
+- Remove caching of events data. !6578
+- Rephrase some system notes to be compatible with new system note style. !7692
+- Pass tag SHA to post-receive hook when tag is created via UI. !7700
+- Prevent error when submitting a merge request and pipeline is not defined. !7707
+- Fixes system note style in commit discussion. !7721
+- Use a Redis lease for updating authorized projects. !7733
+- Refactor JiraService by moving code out of JiraService#execute method. !7756
+- Update GitLab Workhorse to v1.0.1. !7759
+- Fix pipelines info being hidden in merge request widget. !7808
+- Fixed commit timeago not rendering after initial page.
+- Fix for error thrown in cycle analytics events if build has not started.
+- Fixed issue boards issue sorting when dragging issue into list.
+- Allow access to the wiki with git when repository feature disabled.
+- Fixed timeago not rendering when resolving a discussion.
+- Update Sidekiq-cron to fix compatibility issues with Sidekiq 4.2.1.
+- Timeout creating and viewing merge request for binary file.
+- Gracefully recover from Redis connection failures in Sidekiq initializer.
+
+## 8.14.1 (2016-11-28)
+
+- Fix deselecting calendar days on contribution graph. !6453 (ClemMakesApps)
+- Update grape entity to 0.6.0. !7491
+- If Build running change accept merge request when build succeeds button from orange to blue. !7577
+- Changed import sources buttons to checkboxes. !7598 (Luke "Jared" Bennett)
+- Last minute CI Style tweaks for 8.14. !7643
+- Fix exceptions when loading build trace. !7658
+- Fix wrong template rendered when CI/CD settings aren't update successfully. !7665
+- fixes last_deployment call environment is nil. !7671
+- Sort builds by name within pipeline graph. !7681
+- Correctly determine mergeability of MR with no discussions.
+- Sidekiq stats in the admin area will now show correctly on different platforms. (blackst0ne)
+- Fixed issue boards dragging card removing random issues.
+- Fix information disclosure in `Projects::BlobController#update`.
+- Fix missing access checks on issue lookup using IssuableFinder.
+- Replace issue access checks with use of IssuableFinder.
+- Non members cannot create labels through the API.
+- Fix cycle analytics plan stage when commits are missing.
+
## 8.14.0 (2016-11-22)
+- Use separate email-token for incoming email and revert back the inactive feature. !5914
+- API: allow recursive tree request. !6088 (Rebeca Mendez)
+- Replace jQuery.timeago with timeago.js. !6274 (ClemMakesApps)
+- Add CI notifications. Who triggered a pipeline would receive an email after the pipeline is succeeded or failed. Users could also update notification settings accordingly. !6342
+- Add button to delete all merged branches. !6449 (Toon Claes)
+- Finer-grained Git gargage collection. !6588
+- Introduce better credential and error checking to `rake gitlab:ldap:check`. !6601
+- Centralize LDAP config/filter logic. !6606
+- Make system notes less intrusive. !6755
+- Process commits using a dedicated Sidekiq worker. !6802
+- Show random messages when the To Do list is empty. !6818 (Josep Llaneras)
+- Precalculate user's authorized projects in database. !6839
+- Fix record not found error on NewNoteWorker processing. !6863 (Oswaldo Ferreira)
+- Show avatars in mention dropdown. !6865
+- Fix expanding a collapsed diff when converting a symlink to a regular file. !6953
+- Defer saving project services to the database if there are no user changes. !6958
+- Omniauth auto link LDAP user falls back to find by DN when user cannot be found by UID. !7002
+- Display "folders" for environments. !7015
+- Make it possible to trigger builds from webhooks. !7022 (Dmitry Poray)
+- Fix showing pipeline status for a given commit from correct branch. !7034
+- Add link to build pipeline within individual build pages. !7082
+- Add api endpoint `/groups/owned`. !7103 (Borja Aparicio)
+- Add query param to filter users by external & blocked type. !7109 (Yatish Mehta)
+- Issues atom feed url reflect filters on dashboard. !7114 (Lucas Deschamps)
+- Add setting to only allow merge requests to be merged when all discussions are resolved. !7125 (Rodolfo Arruda)
+- Remove an extra leading space from diff paste data. !7133 (Hiroyuki Sato)
+- Fix trace patching feature - update the updated_at value. !7146
+- Fix 404 on network page when entering non-existent git revision. !7172 (Hiroyuki Sato)
+- Rewrite git blame spinach feature tests to rspec feature tests. !7197 (Lisanne Fellinger)
+- Add api endpoint for creating a pipeline. !7209 (Ido Leibovich)
+- Allow users to subscribe to group labels. !7215
+- Reduce API calls needed when importing issues and pull requests from GitHub. !7241 (Andrew Smith (EspadaV8))
+- Only skip group when it's actually a group in the "Share with group" select. !7262
+- Introduce round-robin project creation to spread load over multiple shards. !7266
+- Ensure merge request's "remove branch" accessors return booleans. !7267
+- Fix no "Register" tab if ldap auth is enabled (#24038). !7274 (Luc Didry)
+- Expose label IDs in API. !7275 (Rares Sfirlogea)
+- Fix invalid filename validation on eslint. !7281
+- API: Ability to retrieve version information. !7286 (Robert Schilling)
+- Added ability to throttle Sidekiq Jobs. !7292
+- Set default Sidekiq retries to 3. !7294
+- Fix double event and ajax request call on MR page. !7298 (YarNayar)
+- Unify anchor link format for MR diff files. !7298 (YarNayar)
+- Require projects before creating milestone. !7301 (gfyoung)
+- Fix error when using invalid branch name when creating a new pipeline. !7324
+- Return 400 when creating a system hook fails. !7350 (Robert Schilling)
+- Auto-close environment when branch is deleted. !7355
+- Rework cache invalidation so only changed data is refreshed. !7360
+- Navigation bar issuables counters reflects dashboard issuables counters. !7368 (Lucas Deschamps)
+- Fix cache for commit status in commits list to respect branches. !7372
+- fixes 500 error on project show when user is not logged in and project is still empty. !7376
+- Removed gray button styling from todo buttons in sidebars. !7387
+- Fix project records with invalid visibility_level values. !7391
+- Use 'Forking in progress' title when appropriate. !7394 (Philip Karpiak)
+- Fix error links in help index page. !7396 (Fu Xu)
+- Add support for reply-by-email when the email only contains HTML. !7397
+- [Fix] Extra divider issue in dropdown. !7398
+- Project download buttons always show. !7405 (Philip Karpiak)
+- Give search-input correct padding-right value. !7407 (Philip Karpiak)
+- Remove additional padding on right-aligned items in MR widget. !7411 (Didem Acet)
+- Fix issue causing Labels not to appear in sidebar on MR page. !7416 (Alex Sanford)
+- Allow mail_room idle_timeout option to be configurable. !7423
+- Fix misaligned buttons on admin builds page. !7424 (Didem Acet)
+- Disable "Request Access" functionality by default for new projects and groups. !7425
+- fix shibboleth misconfigurations resulting in authentication bypass. !7428
+- Added Mattermost slash command. !7438
+- Allow to connect Chat account with GitLab. !7450
+- Make New Group form respect default visibility application setting. !7454 (Jacopo Beschi @jacopo-beschi)
+- Fix Error 500 when creating a merge request that contains an image that was deleted and added. !7457
+- Fix labels API by adding missing current_user parameter. !7458 (Francesco Coda Zabetta)
+- Changed restricted visibility admin buttons to checkboxes. !7463
+- Send credentials (currently for registry only) with build data to GitLab Runner. !7474
+- Fix POST /internal/allowed to cope with gitlab-shell v4.0.0 project paths. !7480
+- Adds es6-promise Polyfill. !7482
+- Added colored labels to related MR list. !7486 (Didem Acet)
+- Use setter for key instead AR callback. !7488 (Semyon Pupkov)
+- Limit labels returned for a specific project as an administrator. !7496
+- Change slack notification comment link. !7498 (Herbert Kagumba)
+- Allow registering users whose username contains dots. !7500 (Timothy Andrew)
+- Fix race condition during group deletion and remove stale records present due to this bug. !7528 (Timothy Andrew)
+- Check all namespaces on validation of new username. !7537
+- Pass correct tag target to post-receive hook when creating tag via UI. !7556
+- Add help message for configuring Mattermost slash commands. !7558
+- Fix typo in Build page JavaScript. !7563 (winniehell)
+- Make job script a required configuration entry. !7566
+- Fix errors happening when source branch of merge request is removed and then restored. !7568
+- Fix a wrong "The build for this merge request failed" message. !7579
+- Fix Margins look weird in Project page with pinned sidebar in project stats bar. !7580
+- Fix regression causing bad error message to appear on Merge Request form. !7599 (Alex Sanford)
+- Fix activity page endless scroll on large viewports. !7608
+- Fix 404 on some group pages when name contains dot. !7614
+- Do not create a new TODO when failed build is allowed to fail. !7618
+- Add deployment command to ChatOps. !7619
+- Fix 500 error when group name ends with git. !7630
+- Fix undefined error in CI linter. !7650
+- Show events per stage on Cycle Analytics page. !23449
+- Add JIRA remotelinks and prevent duplicated closing messages.
+- Fixed issue boards counter border when unauthorized.
+- Add placeholder for the example text for custom hex color on label creation popup. (Luis Alonso Chavez Armendariz)
+- Add an index for project_id in project_import_data to improve performance.
+- Fix broken commits search.
+- Assignee dropdown now searches author of issue or merge request.
+- Clicking "force remove source branch" label now toggles the checkbox again.
+- More aggressively preload on merge request and issue index pages.
+- Fix broken link to observatory cli on Frontend Dev Guide. (Sam Rose)
+- Fixing the issue of the project fork url giving 500 when not signed instead of being redirected to sign in page. (Cagdas Gerede)
+- Fix: Guest sees some repository details and gets 404.
+- Add logging for rack attack events to production.log.
+- Add environment info to builds page.
+- Allow commit note to be visible if repo is visible.
+- Bump omniauth-gitlab to 1.0.2 to fix incompatibility with omniauth-oauth2.
+- Redesign pipelines page.
+- Faster search inside Project.
+- Search for a filename in a project.
+- Allow sorting groups in the API.
+- Fix: Todos Filter Shows All Users.
+- Use the Gitlab Workhorse HTTP header in the admin dashboard. (Chris Wright)
+- Fixed multiple requests sent when opening dropdowns.
+- Added permissions per stage to cycle analytics endpoint.
+- Fix project Visibility Level selector not using default values.
+- Add events per stage to cycle analytics.
+- Allow to test JIRA service settings without having a repository.
+- Fix JIRA references for project snippets.
+- Allow enabling and disabling commit and MR events for JIRA.
+- simplify url generation. (Jarka Kadlecova)
- Show correct environment log in admin/logs (@duk3luk3 !7191)
- Fix Milestone dropdown not stay selected for `Upcoming` and `No Milestone` option !7117
- Diff collapse won't shift when collapsing.
@@ -12,6 +199,7 @@ entry.
- Trim leading and trailing whitespace on project_path (Linus Thiel)
- Prevent award emoji via notes for issues/MRs authored by user (barthc)
- Adds support for the `token` attribute in project hooks API (Gauvain Pocentek)
+- Change auto selection behaviour of emoji and slash commands to be more UX/Type friendly (Yann Gravrand)
- Adds an optional path parameter to the Commits API to filter commits by path (Luis HGO)
- Fix Markdown styling inside reference links (Jan Zdráhal)
- Create new issue board list after creating a new label
@@ -19,6 +207,7 @@ entry.
- Fail gracefully when creating merge request with non-existing branch (alexsanford)
- Fix mobile layout issues in admin user overview page !7087
- Fix HipChat notifications rendering (airatshigapov, eisnerd)
+- Removed unneeded "Builds" and "Environments" link from project titles
- Remove 'Edit' button from wiki edit view !7143 (Hiroyuki Sato)
- Cleaned up global namespace JS !19661 (Jose Ivan Vargas)
- Refactor Jira service to use jira-ruby gem
@@ -30,6 +219,7 @@ entry.
- Fix sidekiq stats in admin area (blackst0ne)
- Added label description as tooltip to issue board list title
- Created cycle analytics bundle JavaScript file
+- Make the milestone page more responsive (yury-n)
- Hides container registry when repository is disabled
- API: Fix booleans not recognized as such when using the `to_boolean` helper
- Removed delete branch tooltip !6954
@@ -71,15 +261,68 @@ entry.
- Fix applying GitHub-imported labels when importing job is interrupted
- Allow to search for user by secondary email address in the admin interface(/admin/users) !7115 (YarNayar)
- Updated commit SHA styling on the branches page.
+- Fix "Without projects" filter. !6611 (Ben Bodenmiller)
- Fix 404 when visit /projects page
+## 8.13.9 (2016-12-08)
+
+- Reenables /user API request to return private-token if user is admin and request is made with sudo. !7615
+- Replace MR access checks with use of MergeRequestsFinder.
+
+## 8.13.8 (2016-12-02)
+
+- Pass tag SHA to post-receive hook when tag is created via UI. !7700
+- Validate state param when filtering issuables.
+
+## 8.13.7 (2016-11-28)
+
+- fixes 500 error on project show when user is not logged in and project is still empty. !7376
+- Update grape entity to 0.6.0. !7491
+- Fix information disclosure in `Projects::BlobController#update`.
+- Fix missing access checks on issue lookup using IssuableFinder.
+- Replace issue access checks with use of IssuableFinder.
+- Non members cannot create labels through the API.
+
+## 8.13.6 (2016-11-17)
+
+- Omniauth auto link LDAP user falls back to find by DN when user cannot be found by UID. !7002
+- Fix Milestone dropdown not stay selected for `Upcoming` and `No Milestone` option. !7117
+- Fix relative links in Markdown wiki when displayed in "Project" tab. !7218
+- Fix no "Register" tab if ldap auth is enabled (#24038). !7274 (Luc Didry)
+- Fix cache for commit status in commits list to respect branches. !7372
+- Fix issue causing Labels not to appear in sidebar on MR page. !7416 (Alex Sanford)
+- Limit labels returned for a specific project as an administrator. !7496
+- Clicking "force remove source branch" label now toggles the checkbox again.
+- Allow commit note to be visible if repo is visible.
+- Fix project Visibility Level selector not using default values.
+
+## 8.13.5 (2016-11-08)
+
+- Restore unauthenticated access to public container registries
+- Fix showing pipeline status for a given commit from correct branch. !7034
+- Only skip group when it's actually a group in the "Share with group" select. !7262
+- Introduce round-robin project creation to spread load over multiple shards. !7266
+- Ensure merge request's "remove branch" accessors return booleans. !7267
+- Ensure external users are not able to clone disabled repositories.
+- Fix XSS issue in Markdown autolinker.
+- Respect event visibility in Gitlab::ContributionsCalendar.
+- Honour issue and merge request visibility in their respective finders.
+- Disable reference Markdown for unavailable features.
+- Fix lightweight tags not processed correctly by GitTagPushService. !6532
+- Allow owners to fetch source code in CI builds. !6943
+- Return conflict error in label API when title is taken by group label. !7014
+- Reduce the overhead to calculate number of open/closed issues and merge requests within the group or project. !7123
+- Fix builds tab visibility. !7178
+- Fix project features default values. !7181
+
+## 8.13.4
+
+- Pulled due to packaging error.
+
## 8.13.3 (2016-11-02)
- Removes any symlinks before importing a project export file. CVE-2016-9086
- Fixed Import/Export foreign key issue to do with project members.
-- Fix relative links in Markdown wiki when displayed in "Project" tab !7218
-- Reduce the overhead to calculate number of open/closed issues and merge requests within the group or project
-- Fix project features default values
- Changed build dropdown list length to be 6,5 builds long in the pipeline graph
## 8.13.2 (2016-10-31)
@@ -270,6 +513,25 @@ entry.
- Fix broken Project API docs (Takuya Noguchi)
- Migrate invalid project members (owner -> master)
+## 8.12.12 (2016-12-08)
+
+- Replace MR access checks with use of MergeRequestsFinder
+- Reenables /user API request to return private-token if user is admin and request is made with sudo
+
+## 8.12.11 (2016-12-02)
+
+- No changes
+
+## 8.12.10 (2016-11-28)
+
+- Fix information disclosure in `Projects::BlobController#update`
+- Fix missing access checks on issue lookup using IssuableFinder
+- Replace issue access checks with use of IssuableFinder
+
+## 8.12.9 (2016-11-07)
+
+- Fix XSS issue in Markdown autolinker
+
## 8.12.8 (2016-11-02)
- Removes any symlinks before importing a project export file. CVE-2016-9086
@@ -534,6 +796,10 @@ entry.
- Fix non-master branch readme display in tree view
- Add UX improvements for merge request version diffs
+## 8.11.11 (2016-11-07)
+
+- Fix XSS issue in Markdown autolinker
+
## 8.11.10 (2016-11-02)
- Removes any symlinks before importing a project export file. CVE-2016-9086
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 67c30c2424c..659871a06a4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,8 +9,6 @@
- [Helping others](#helping-others)
- [I want to contribute!](#i-want-to-contribute)
- [Implement design & UI elements](#implement-design-ui-elements)
- - [Design reference](#design-reference)
- - [UI development kit](#ui-development-kit)
- [Issue tracker](#issue-tracker)
- [Feature proposals](#feature-proposals)
- [Issue tracker guidelines](#issue-tracker-guidelines)
@@ -90,7 +88,7 @@ This was inspired by [an article by Kent C. Dodds][medium-up-for-grabs].
## Implement design & UI elements
-Please see the [UI Guide for building GitLab].
+Please see the [UX Guide for GitLab].
## Issue tracker
@@ -218,7 +216,10 @@ associated with in the description of the issue.
We welcome merge requests with fixes and improvements to GitLab code, tests,
and/or documentation. The features we would really like a merge request for are
listed with the label [`Accepting Merge Requests` on our issue tracker for CE][accepting-mrs-ce]
-and [EE][accepting-mrs-ee] but other improvements are also welcome.
+and [EE][accepting-mrs-ee] but other improvements are also welcome. Please note
+that if an issue is marked for the current milestone either before or while you
+are working on it, a team member may take over the merge request in order to
+ensure the work is finished before the release date.
If you want to add a new feature that is not labeled it is best to first create
a feedback issue (if there isn't one already) and leave a comment asking for it
@@ -469,5 +470,5 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide"
[scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide"
[newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide"
-[UI Guide for building GitLab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/ui_guide.md
+[UX Guide for GitLab]: http://docs.gitlab.com/ce/development/ux_guide/
[license-finder-doc]: doc/development/licensing.md
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index fcdb2e109f6..ee74734aa22 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-4.0.0
+4.1.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 3eefcb9dd5b..26aaba0e866 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-1.0.0
+1.2.0
diff --git a/Gemfile b/Gemfile
index cb2a8470126..bea31b53b1c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -22,7 +22,6 @@ gem 'doorkeeper', '~> 4.2.0'
gem 'omniauth', '~> 1.3.1'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
-gem 'omniauth-bitbucket', '~> 0.0.2'
gem 'omniauth-cas3', '~> 1.1.2'
gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1'
@@ -67,8 +66,8 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
gem 'github-linguist', '~> 4.7.0', require: 'linguist'
# API
-gem 'grape', '~> 0.15.0'
-gem 'grape-entity', '~> 0.4.2'
+gem 'grape', '~> 0.18.0'
+gem 'grape-entity', '~> 0.6.0'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
# Pagination
@@ -85,10 +84,8 @@ gem 'dropzonejs-rails', '~> 0.7.1'
# for backups
gem 'fog-aws', '~> 0.9'
-gem 'fog-azure', '~> 0.0'
gem 'fog-core', '~> 1.40'
gem 'fog-local', '~> 0.3'
-gem 'fog-google', '~> 0.3'
gem 'fog-openstack', '~> 0.1'
gem 'fog-rackspace', '~> 0.1.1'
@@ -134,9 +131,10 @@ gem 'after_commit_queue', '~> 1.3.0'
gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs
-gem 'sidekiq', '~> 4.2'
-gem 'sidekiq-cron', '~> 0.4.0'
+gem 'sidekiq', '~> 4.2.7'
+gem 'sidekiq-cron', '~> 0.4.4'
gem 'redis-namespace', '~> 1.5.2'
+gem 'sidekiq-limit_fetch', '~> 3.4'
# HTTP requests
gem 'httparty', '~> 0.13.3'
@@ -179,6 +177,9 @@ gem 'asana', '~> 0.4.0'
# FogBugz integration
gem 'ruby-fogbugz', '~> 0.2.1'
+# Kubernetes integration
+gem 'kubeclient', '~> 2.2.0'
+
# d3
gem 'd3_rails', '~> 3.5.0'
@@ -265,14 +266,14 @@ group :development do
end
group :development, :test do
- gem 'byebug', '~> 8.2.1', platform: :mri
+ gem 'pry-byebug', '~> 3.4.1', platform: :mri
gem 'pry-rails', '~> 0.3.4'
gem 'awesome_print', '~> 1.2.0', require: false
gem 'fuubar', '~> 2.0.0'
gem 'database_cleaner', '~> 1.5.0'
- gem 'factory_girl_rails', '~> 4.6.0'
+ gem 'factory_girl_rails', '~> 4.7.0'
gem 'rspec-rails', '~> 3.5.0'
gem 'rspec-retry', '~> 0.4.5'
gem 'spinach-rails', '~> 0.2.1'
@@ -310,6 +311,8 @@ group :development, :test do
gem 'knapsack', '~> 1.11.0'
gem 'activerecord_sane_schema_dumper', '0.2'
+
+ gem 'stackprof', '~> 0.2.10'
end
group :test do
@@ -329,18 +332,15 @@ gem 'octokit', '~> 4.3.0'
gem 'mail_room', '~> 0.9.0'
gem 'email_reply_parser', '~> 0.5.8'
+gem 'html2text'
gem 'ruby-prof', '~> 0.16.2'
-## CI
-gem 'activerecord-session_store', '~> 1.0.0'
-gem 'nested_form', '~> 0.3.2'
-
# OAuth
gem 'oauth2', '~> 1.2.0'
# Soft deletion
-gem 'paranoia', '~> 2.0'
+gem 'paranoia', '~> 2.2'
# Health check
gem 'health_check', '~> 2.2.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 290e4c3e1b3..811adfc5c1d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -32,12 +32,6 @@ GEM
activemodel (= 4.2.7.1)
activesupport (= 4.2.7.1)
arel (~> 6.0)
- activerecord-session_store (1.0.0)
- actionpack (>= 4.0, < 5.1)
- activerecord (>= 4.0, < 5.1)
- multi_json (~> 1.11, >= 1.11.2)
- rack (>= 1.5.2, < 3)
- railties (>= 4.0, < 5.1)
activerecord_sane_schema_dumper (0.2)
rails (>= 4, < 5)
activesupport (4.2.7.1)
@@ -72,21 +66,6 @@ GEM
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
- azure (0.7.5)
- addressable (~> 2.3)
- azure-core (~> 0.1)
- faraday (~> 0.9)
- faraday_middleware (~> 0.10)
- json (~> 1.8)
- mime-types (>= 1, < 3.0)
- nokogiri (~> 1.6)
- systemu (~> 2.6)
- thor (~> 0.19)
- uuid (~> 2.0)
- azure-core (0.1.2)
- faraday (~> 0.9)
- faraday_middleware (~> 0.10)
- nokogiri (~> 1.6)
babel-source (5.8.35)
babel-transpiler (0.7.0)
babel-source (>= 4.0, < 6)
@@ -112,7 +91,7 @@ GEM
bundler-audit (0.5.0)
bundler (~> 1.2)
thor (~> 0.18)
- byebug (8.2.1)
+ byebug (9.0.6)
capybara (2.6.2)
addressable
mime-types (>= 1.16)
@@ -147,7 +126,7 @@ GEM
coffee-script-source (1.10.0)
colorize (0.7.7)
concurrent-ruby (1.0.2)
- connection_pool (2.2.0)
+ connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
creole (0.5.0)
@@ -182,6 +161,8 @@ GEM
diff-lcs (1.2.5)
diffy (3.1.0)
docile (1.1.5)
+ domain_name (0.5.20161021)
+ unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.0)
railties (>= 4.2)
dropzonejs-rails (0.7.2)
@@ -198,10 +179,10 @@ GEM
excon (0.52.0)
execjs (2.6.0)
expression_parser (0.9.0)
- factory_girl (4.5.0)
+ factory_girl (4.7.0)
activesupport (>= 3.0.0)
- factory_girl_rails (4.6.0)
- factory_girl (~> 4.5.0)
+ factory_girl_rails (4.7.0)
+ factory_girl (~> 4.7.0)
railties (>= 3.0.0)
faraday (0.9.2)
multipart-post (>= 1.2, < 3)
@@ -223,19 +204,10 @@ GEM
fog-json (~> 1.0)
fog-xml (~> 0.1)
ipaddress (~> 0.8)
- fog-azure (0.0.2)
- azure (~> 0.6)
- fog-core (~> 1.27)
- fog-json (~> 1.0)
- fog-xml (~> 0.1)
fog-core (1.42.0)
builder
excon (~> 0.49)
formatador (~> 0.2)
- fog-google (0.3.2)
- fog-core
- fog-json
- fog-xml
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
@@ -312,17 +284,17 @@ GEM
json
multi_json
request_store (>= 1.0)
- grape (0.15.0)
+ grape (0.18.0)
activesupport
builder
hashie (>= 2.1.0)
multi_json (>= 1.3.2)
multi_xml (>= 0.5.2)
+ mustermann-grape (~> 0.4.0)
rack (>= 1.3.0)
rack-accept
- rack-mount
virtus (>= 1.0.0)
- grape-entity (0.4.8)
+ grape-entity (0.6.0)
activesupport
multi_json (>= 1.3.2)
haml (4.0.7)
@@ -345,7 +317,18 @@ GEM
html-pipeline (1.11.0)
activesupport (>= 2)
nokogiri (~> 1.4)
+ html2text (0.2.0)
+ nokogiri (~> 1.6)
htmlentities (4.3.4)
+ http (0.9.8)
+ addressable (~> 2.3)
+ http-cookie (~> 1.0)
+ http-form_data (~> 1.0.1)
+ http_parser.rb (~> 0.6.0)
+ http-cookie (1.0.3)
+ domain_name (~> 0.5)
+ http-form_data (1.0.1)
+ http_parser.rb (0.6.0)
httparty (0.13.7)
json (~> 1.8)
multi_xml (>= 0.5.2)
@@ -380,6 +363,10 @@ GEM
knapsack (1.11.0)
rake
timecop (>= 0.1.0)
+ kubeclient (2.2.0)
+ http (= 0.9.8)
+ recursive-open-struct (= 1.0.0)
+ rest-client
launchy (2.4.3)
addressable (~> 2.3)
letter_opener (1.4.1)
@@ -401,8 +388,6 @@ GEM
rb-inotify (>= 0.9)
loofah (2.0.3)
nokogiri (>= 1.5.9)
- macaddr (1.7.1)
- systemu (~> 2.6.2)
mail (2.6.4)
mime-types (>= 1.16, < 4)
mail_room (0.9.0)
@@ -415,10 +400,14 @@ GEM
multi_json (1.12.1)
multi_xml (0.5.5)
multipart-post (2.0.0)
+ mustermann (0.4.0)
+ tool (~> 0.2)
+ mustermann-grape (0.4.0)
+ mustermann (= 0.4.0)
mysql2 (0.3.20)
- nested_form (0.3.2)
net-ldap (0.12.1)
net-ssh (3.0.1)
+ netrc (0.11.0)
newrelic_rpm (3.16.0.318)
nokogiri (1.6.8)
mini_portile2 (~> 2.1.0)
@@ -443,10 +432,6 @@ GEM
jwt (~> 1.0)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1)
- omniauth-bitbucket (0.0.2)
- multi_json (~> 1.7)
- omniauth (~> 1.1)
- omniauth-oauth (~> 1.0)
omniauth-cas3 (1.1.3)
addressable (~> 2.3)
nokogiri (~> 1.6.6)
@@ -491,8 +476,8 @@ GEM
org-ruby (0.9.12)
rubypants (~> 0.2)
orm_adapter (0.5.0)
- paranoia (2.1.4)
- activerecord (~> 4.0)
+ paranoia (2.2.0)
+ activerecord (>= 4.0, < 5.1)
parser (2.3.1.4)
ast (~> 2.2)
pg (0.18.4)
@@ -514,17 +499,18 @@ GEM
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
+ pry-byebug (3.4.1)
+ byebug (~> 9.0)
+ pry (~> 0.10)
pry-rails (0.3.4)
pry (>= 0.9.10)
pyu-ruby-sasl (0.0.3.3)
- rack (1.6.4)
+ rack (1.6.5)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (4.4.1)
rack
rack-cors (0.4.0)
- rack-mount (0.8.3)
- rack (>= 1.0.0)
rack-oauth2 (1.2.3)
activesupport (>= 2.3)
attr_required (>= 0.0.5)
@@ -571,6 +557,7 @@ GEM
json (~> 1.4)
recaptcha (3.0.0)
json
+ recursive-open-struct (1.0.0)
redcarpet (3.3.3)
redis (3.2.2)
redis-actionpack (5.0.1)
@@ -596,9 +583,13 @@ GEM
listen (~> 3.0)
responders (2.3.0)
railties (>= 4.2.0, < 5.1)
+ rest-client (2.0.0)
+ http-cookie (>= 1.0.2, < 2.0)
+ mime-types (>= 1.16, < 4.0)
+ netrc (~> 0.8)
rinku (2.0.0)
rotp (2.1.2)
- rouge (2.0.6)
+ rouge (2.0.7)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
@@ -676,15 +667,17 @@ GEM
rack
shoulda-matchers (2.8.0)
activesupport (>= 3.0.0)
- sidekiq (4.2.1)
+ sidekiq (4.2.7)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
- rack-protection (~> 1.5)
+ rack-protection (>= 1.5.0)
redis (~> 3.2, >= 3.2.1)
- sidekiq-cron (0.4.0)
+ sidekiq-cron (0.4.4)
redis-namespace (>= 1.5.2)
rufus-scheduler (>= 2.0.24)
- sidekiq (>= 4.0.0)
+ sidekiq (>= 4.2.1)
+ sidekiq-limit_fetch (3.4.0)
+ sidekiq (>= 4)
simplecov (0.12.0)
docile (~> 1.1.0)
json (>= 1.8, < 3)
@@ -720,6 +713,7 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
+ stackprof (0.2.10)
state_machines (0.4.0)
state_machines-activemodel (0.4.0)
activemodel (>= 4.1, < 5.1)
@@ -731,7 +725,6 @@ GEM
sys-filesystem (1.1.6)
ffi
sysexits (1.2.0)
- systemu (2.6.5)
teaspoon (1.1.5)
railties (>= 3.2.5, < 6)
teaspoon-jasmine (2.2.0)
@@ -748,6 +741,7 @@ GEM
tilt (2.0.5)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
+ tool (0.2.3)
truncato (0.7.8)
htmlentities (~> 4.3.1)
nokogiri (~> 1.6.1)
@@ -771,8 +765,6 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
- uuid (2.3.8)
- macaddr (~> 1.0)
version_sorter (2.1.0)
virtus (1.0.5)
axiom-types (~> 0.1)
@@ -807,7 +799,6 @@ PLATFORMS
DEPENDENCIES
RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0)
- activerecord-session_store (~> 1.0.0)
activerecord_sane_schema_dumper (= 0.2)
acts-as-taggable-on (~> 4.0)
addressable (~> 2.3.8)
@@ -828,7 +819,6 @@ DEPENDENCIES
browser (~> 2.2)
bullet (~> 5.2.0)
bundler-audit (~> 0.5.0)
- byebug (~> 8.2.1)
capybara (~> 2.6.2)
capybara-screenshot (~> 1.0.0)
carrierwave (~> 0.10.0)
@@ -849,13 +839,11 @@ DEPENDENCIES
dropzonejs-rails (~> 0.7.1)
email_reply_parser (~> 0.5.8)
email_spec (~> 1.6.0)
- factory_girl_rails (~> 4.6.0)
+ factory_girl_rails (~> 4.7.0)
ffaker (~> 2.0.0)
flay (~> 2.6.1)
fog-aws (~> 0.9)
- fog-azure (~> 0.0)
fog-core (~> 1.40)
- fog-google (~> 0.3)
fog-local (~> 0.3)
fog-openstack (~> 0.1)
fog-rackspace (~> 0.1.1)
@@ -872,13 +860,14 @@ DEPENDENCIES
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2)
gon (~> 6.1.0)
- grape (~> 0.15.0)
- grape-entity (~> 0.4.2)
+ grape (~> 0.18.0)
+ grape-entity (~> 0.6.0)
haml_lint (~> 0.18.2)
hamlit (~> 2.6.1)
health_check (~> 2.2.0)
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
+ html2text
httparty (~> 0.13.3)
influxdb (~> 0.2)
jira-ruby (~> 1.1.2)
@@ -890,6 +879,7 @@ DEPENDENCIES
jwt
kaminari (~> 0.17.0)
knapsack (~> 1.11.0)
+ kubeclient (~> 2.2.0)
letter_opener_web (~> 1.3.0)
license_finder (~> 2.1.0)
licensee (~> 8.0.0)
@@ -899,7 +889,6 @@ DEPENDENCIES
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.3.16)
- nested_form (~> 0.3.2)
net-ssh (~> 3.0.1)
newrelic_rpm (~> 3.16)
nokogiri (~> 1.6.7, >= 1.6.7.2)
@@ -909,7 +898,6 @@ DEPENDENCIES
omniauth (~> 1.3.1)
omniauth-auth0 (~> 1.4.1)
omniauth-azure-oauth2 (~> 0.0.6)
- omniauth-bitbucket (~> 0.0.2)
omniauth-cas3 (~> 1.1.2)
omniauth-facebook (~> 4.0.0)
omniauth-github (~> 1.1.1)
@@ -921,10 +909,11 @@ DEPENDENCIES
omniauth-twitter (~> 1.2.0)
omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12)
- paranoia (~> 2.0)
+ paranoia (~> 2.2)
pg (~> 0.18.2)
poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.0)
+ pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
rack-cors (~> 0.4.0)
@@ -959,8 +948,9 @@ DEPENDENCIES
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0)
- sidekiq (~> 4.2)
- sidekiq-cron (~> 0.4.0)
+ sidekiq (~> 4.2.7)
+ sidekiq-cron (~> 0.4.4)
+ sidekiq-limit_fetch (~> 3.4)
simplecov (= 0.12.0)
slack-notifier (~> 1.2.0)
spinach-rails (~> 0.2.1)
@@ -971,6 +961,7 @@ DEPENDENCIES
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)
diff --git a/README.md b/README.md
index dbe6db3ebed..68b709b85d5 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# GitLab
[![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
-[![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](http://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
+[![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
@@ -76,12 +76,16 @@ GitLab is a Ruby on Rails application that runs on the following software:
- Ubuntu/Debian/CentOS/RHEL
- Ruby (MRI) 2.3
-- Git 2.7.4+
+- Git 2.8.4+
- Redis 2.8+
- MySQL or PostgreSQL
For more information please see the [architecture documentation](https://docs.gitlab.com/ce/development/architecture.html).
+## UX design
+
+Please adhere to the [UX Guide](doc/development/ux_guide/index.md) when creating designs and implementing code.
+
## Third-party applications
There are a lot of [third-party applications integrating with GitLab](https://about.gitlab.com/applications/). These include GUI Git clients, mobile applications and API wrappers for various languages.
diff --git a/VERSION b/VERSION
index 919f462addc..d59bc5cbc5c 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.14.0-pre
+8.15.0-pre
diff --git a/app/assets/fonts/OFL.txt b/app/assets/fonts/OFL.txt
deleted file mode 100644
index df187637e18..00000000000
--- a/app/assets/fonts/OFL.txt
+++ /dev/null
@@ -1,93 +0,0 @@
-Copyright 2010, 2012, 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
-
-This Font Software is licensed under the SIL Open Font License, Version 1.1.
-
-This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
-
-
------------------------------------------------------------
-SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
------------------------------------------------------------
-
-PREAMBLE
-The goals of the Open Font License (OFL) are to stimulate worldwide
-development of collaborative font projects, to support the font creation
-efforts of academic and linguistic communities, and to provide a free and
-open framework in which fonts may be shared and improved in partnership
-with others.
-
-The OFL allows the licensed fonts to be used, studied, modified and
-redistributed freely as long as they are not sold by themselves. The
-fonts, including any derivative works, can be bundled, embedded,
-redistributed and/or sold with any software provided that any reserved
-names are not used by derivative works. The fonts and derivatives,
-however, cannot be released under any other type of license. The
-requirement for fonts to remain under this license does not apply
-to any document created using the fonts or their derivatives.
-
-DEFINITIONS
-"Font Software" refers to the set of files released by the Copyright
-Holder(s) under this license and clearly marked as such. This may
-include source files, build scripts and documentation.
-
-"Reserved Font Name" refers to any names specified as such after the
-copyright statement(s).
-
-"Original Version" refers to the collection of Font Software components as
-distributed by the Copyright Holder(s).
-
-"Modified Version" refers to any derivative made by adding to, deleting,
-or substituting -- in part or in whole -- any of the components of the
-Original Version, by changing formats or by porting the Font Software to a
-new environment.
-
-"Author" refers to any designer, engineer, programmer, technical
-writer or other person who contributed to the Font Software.
-
-PERMISSION & CONDITIONS
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of the Font Software, to use, study, copy, merge, embed, modify,
-redistribute, and sell modified and unmodified copies of the Font
-Software, subject to the following conditions:
-
-1) Neither the Font Software nor any of its individual components,
-in Original or Modified Versions, may be sold by itself.
-
-2) Original or Modified Versions of the Font Software may be bundled,
-redistributed and/or sold with any software, provided that each copy
-contains the above copyright notice and this license. These can be
-included either as stand-alone text files, human-readable headers or
-in the appropriate machine-readable metadata fields within text or
-binary files as long as those fields can be easily viewed by the user.
-
-3) No Modified Version of the Font Software may use the Reserved Font
-Name(s) unless explicit written permission is granted by the corresponding
-Copyright Holder. This restriction only applies to the primary font name as
-presented to the users.
-
-4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
-Software shall not be used to promote, endorse or advertise any
-Modified Version, except to acknowledge the contribution(s) of the
-Copyright Holder(s) and the Author(s) or with their explicit written
-permission.
-
-5) The Font Software, modified or unmodified, in part or in whole,
-must be distributed entirely under this license, and must not be
-distributed under any other license. The requirement for fonts to
-remain under this license does not apply to any document created
-using the Font Software.
-
-TERMINATION
-This license becomes null and void if any of the above conditions are
-not met.
-
-DISCLAIMER
-THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
-OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
-COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
-DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
-OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/app/assets/fonts/SourceSansPro-Black.ttf.woff b/app/assets/fonts/SourceSansPro-Black.ttf.woff
deleted file mode 100644
index b7e86200927..00000000000
--- a/app/assets/fonts/SourceSansPro-Black.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Black.ttf.woff2 b/app/assets/fonts/SourceSansPro-Black.ttf.woff2
deleted file mode 100644
index c90d078406c..00000000000
--- a/app/assets/fonts/SourceSansPro-Black.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff
deleted file mode 100644
index c3314b1ef06..00000000000
--- a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2
deleted file mode 100644
index b87e22c41b5..00000000000
--- a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf.woff b/app/assets/fonts/SourceSansPro-Bold.ttf.woff
deleted file mode 100644
index d1d40f840f8..00000000000
--- a/app/assets/fonts/SourceSansPro-Bold.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf.woff2 b/app/assets/fonts/SourceSansPro-Bold.ttf.woff2
deleted file mode 100644
index 0f46f3e833a..00000000000
--- a/app/assets/fonts/SourceSansPro-Bold.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff
deleted file mode 100644
index ef6ff514d3a..00000000000
--- a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2
deleted file mode 100644
index 8007df6df32..00000000000
--- a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff
deleted file mode 100644
index 1e6c94d9eb3..00000000000
--- a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2
deleted file mode 100644
index b715f274082..00000000000
--- a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff
deleted file mode 100644
index 7a408b1ec73..00000000000
--- a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2
deleted file mode 100644
index d8f9d29d4aa..00000000000
--- a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-It.ttf.woff b/app/assets/fonts/SourceSansPro-It.ttf.woff
deleted file mode 100644
index 4d54bc95718..00000000000
--- a/app/assets/fonts/SourceSansPro-It.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-It.ttf.woff2 b/app/assets/fonts/SourceSansPro-It.ttf.woff2
deleted file mode 100644
index a00852641f8..00000000000
--- a/app/assets/fonts/SourceSansPro-It.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Light.ttf.woff b/app/assets/fonts/SourceSansPro-Light.ttf.woff
deleted file mode 100644
index 1706d57d3c5..00000000000
--- a/app/assets/fonts/SourceSansPro-Light.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Light.ttf.woff2 b/app/assets/fonts/SourceSansPro-Light.ttf.woff2
deleted file mode 100644
index d8b610ad76e..00000000000
--- a/app/assets/fonts/SourceSansPro-Light.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff
deleted file mode 100644
index 87378d6c609..00000000000
--- a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2
deleted file mode 100644
index e0eebac8273..00000000000
--- a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf.woff b/app/assets/fonts/SourceSansPro-Regular.ttf.woff
deleted file mode 100644
index 460ab12a638..00000000000
--- a/app/assets/fonts/SourceSansPro-Regular.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf.woff2 b/app/assets/fonts/SourceSansPro-Regular.ttf.woff2
deleted file mode 100644
index 0dd3464c74b..00000000000
--- a/app/assets/fonts/SourceSansPro-Regular.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff
deleted file mode 100644
index 43379631b2d..00000000000
--- a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2
deleted file mode 100644
index 2526d2e1b60..00000000000
--- a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff
deleted file mode 100644
index 232c2048ae7..00000000000
--- a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2
deleted file mode 100644
index 606935af089..00000000000
--- a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6
index 82e526ae0ef..8a260aae1b1 100644
--- a/app/assets/javascripts/abuse_reports.js.es6
+++ b/app/assets/javascripts/abuse_reports.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-param-reassign */
+
((global) => {
const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
deleted file mode 100644
index 59ac9b9cef5..00000000000
--- a/app/assets/javascripts/activities.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/* eslint-disable */
-(function() {
- this.Activities = (function() {
- function Activities() {
- Pager.init(20, true, false, this.updateTooltips);
- $(".event-filter-link").on("click", (function(_this) {
- return function(event) {
- event.preventDefault();
- _this.toggleFilter($(event.currentTarget));
- return _this.reloadActivities();
- };
- })(this));
- }
-
- Activities.prototype.updateTooltips = function() {
- return gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
- };
-
- Activities.prototype.reloadActivities = function() {
- $(".content_list").html('');
- return Pager.init(20, true);
- };
-
- Activities.prototype.toggleFilter = function(sender) {
- var filter = sender.attr("id").split("_")[0];
-
- $('.event-filter .active').removeClass("active");
- Cookies.set("event_filter", filter);
-
- sender.closest('li').toggleClass("active");
- };
-
- return Activities;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/activities.js.es6 b/app/assets/javascripts/activities.js.es6
new file mode 100644
index 00000000000..648cb4d5d85
--- /dev/null
+++ b/app/assets/javascripts/activities.js.es6
@@ -0,0 +1,37 @@
+/* eslint-disable no-param-reassign, class-methods-use-this */
+/* global Pager */
+/* global Cookies */
+
+((global) => {
+ class Activities {
+ constructor() {
+ Pager.init(20, true, false, this.updateTooltips);
+ $('.event-filter-link').on('click', (e) => {
+ e.preventDefault();
+ this.toggleFilter(e.currentTarget);
+ this.reloadActivities();
+ });
+ }
+
+ updateTooltips() {
+ gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
+ }
+
+ reloadActivities() {
+ $('.content_list').html('');
+ Pager.init(20, true, false, this.updateTooltips);
+ }
+
+ toggleFilter(sender) {
+ const $sender = $(sender);
+ const filter = $sender.attr('id').split('_')[0];
+
+ $('.event-filter .active').removeClass('active');
+ Cookies.set('event_filter', filter);
+
+ $sender.closest('li').toggleClass('active');
+ }
+ }
+
+ global.Activities = Activities;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index 1ef340e4ca1..5a7d823e84c 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, padded-blocks, max-len */
+/* global Turbolinks */
+
(function() {
this.Admin = (function() {
function Admin() {
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 291efd2e286..f60f27d1210 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, indent, object-curly-spacing, quote-props, no-param-reassign, padded-blocks, max-len */
+
(function() {
- this.Api = {
+ var Api = {
groupsPath: "/api/:version/groups.json",
groupPath: "/api/:version/groups/:id.json",
namespacesPath: "/api/:version/namespaces.json",
@@ -145,4 +146,5 @@
}
};
+ window.Api = Api;
}).call(this);
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 7d942de0184..043c6a11c4f 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -1,4 +1,11 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len */
+/* global bp */
+/* global Cookies */
+/* global Flash */
+/* global ConfirmDangerModal */
+/* 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
@@ -13,7 +20,6 @@
/*= require jquery-ui/sortable */
/*= require jquery_ujs */
/*= require jquery.endless-scroll */
-/*= require jquery.timeago */
/*= require jquery.highlight */
/*= require jquery.waitforimages */
/*= require jquery.atwho */
@@ -54,18 +60,21 @@
/*= require_directory ./u2f */
/*= require_directory . */
/*= require fuzzaldrin-plus */
+/*= require es6-promise.auto */
(function () {
- document.addEventListener('page:fetch', gl.utils.cleanupBeforeFetch);
- window.addEventListener('hashchange', gl.utils.shiftWindow);
+ document.addEventListener('page:fetch', function () {
+ // Unbind scroll events
+ $(document).off('scroll');
+ // Close any open tooltips
+ $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
+ });
- window.onload = function () {
- // Scroll the window to avoid the topnav bar
- // https://github.com/twitter/bootstrap/issues/1768
- if (location.hash) {
- return setTimeout(gl.utils.shiftWindow, 100);
- }
- };
+ window.addEventListener('hashchange', gl.utils.handleLocationHash);
+ window.addEventListener('load', function onLoad() {
+ window.removeEventListener('load', onLoad, false);
+ gl.utils.handleLocationHash();
+ }, false);
$(function () {
var $body = $('body');
@@ -80,7 +89,15 @@
// Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/';
- gl.utils.preventDisabledButtons();
+ // prevent default action for disabled buttons
+ $('.btn').click(function(e) {
+ if ($(this).hasClass('disabled')) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ return false;
+ }
+ });
+
$('.nav-sidebar').niceScroll({
cursoropacitymax: '0.4',
cursorcolor: '#FFF',
@@ -194,9 +211,6 @@
e.preventDefault();
return new ConfirmDangerModal(form, text);
});
- $document.on('click', 'button', function () {
- return $(this).blur();
- });
$('input[type="search"]').each(function () {
var $this = $(this);
$this.attr('value', $this.val());
@@ -238,8 +252,5 @@
// bind sidebar events
new gl.Sidebar();
-
- // Custom time ago
- gl.utils.shortTimeAgo($('.js-short-timeago'));
});
}).call(this);
diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js
index c7eff27f971..9417afc2ea7 100644
--- a/app/assets/javascripts/aside.js
+++ b/app/assets/javascripts/aside.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, prefer-arrow-callback, no-var, one-var, one-var-declaration-per-line, no-else-return, padded-blocks, max-len */
(function() {
this.Aside = (function() {
function Aside() {
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index ab09e4475e6..f45dbe4cbf2 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, padded-blocks, max-len */
(function() {
this.Autosave = (function() {
function Autosave(field, key) {
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index d7cda977845..107a7978a87 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, spaced-comment, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, radix, keyword-spacing, space-before-blocks, brace-style, no-underscore-dangle, no-plusplus, no-return-assign, camelcase, padded-blocks */
+/* global Cookies */
+
(function() {
this.AwardsHandler = (function() {
var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index 074378b9e52..c62a4c5a456 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, padded-blocks, max-len */
+/* global autosize */
/*= require jquery.ba-resize */
/*= require autosize */
diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js
index a64cefb62bd..3998ee9a0a0 100644
--- a/app/assets/javascripts/behaviors/details_behavior.js
+++ b/app/assets/javascripts/behaviors/details_behavior.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, padded-blocks, max-len */
(function() {
$(function() {
$("body").on("click", ".js-details-target", function() {
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 7ff88ecdcaf..586f941a6e3 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, camelcase, consistent-return, quotes, object-shorthand, comma-dangle, padded-blocks, max-len */
+
// Quick Submit behavior
//
// When a child field of a form with a `js-quick-submit` class receives a
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
index 4ac343f876c..72362988b2e 100644
--- a/app/assets/javascripts/behaviors/requires_input.js
+++ b/app/assets/javascripts/behaviors/requires_input.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, no-else-return, consistent-return, padded-blocks, max-len */
// Requires Input behavior
//
// When called on a form with input fields with the `required` attribute, the
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 05b213fe3fb..6a49715590c 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, vars-on-top, no-var, max-len */
(function(w) {
$(function() {
// Toggle button. Show/hide content inside parent container.
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
index 37531aaec9b..57bd13eecf8 100644
--- a/app/assets/javascripts/blob/blob_ci_yaml.js.es6
+++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */
+/* global Api */
+
/*= require blob/template_selector */
((global) => {
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index 33fb4f8185c..eab686c45c3 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, padded-blocks, max-len */
+/* global Dropzone */
+
(function() {
this.BlobFileDropzone = (function() {
function BlobFileDropzone(form, method) {
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js
index 344fe5dcd94..15563e429a0 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selector.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selector.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, padded-blocks */
+/* global Api */
/*= require blob/template_selector */
diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js
index 9e992f7913c..d7f95093688 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selectors.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, comma-dangle, padded-blocks, max-len */
+/* global BlobGitignoreSelector */
+
(function() {
this.BlobGitignoreSelectors = (function() {
function BlobGitignoreSelectors(opts) {
diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js
index 41a83a56146..d9c6f65a083 100644
--- a/app/assets/javascripts/blob/blob_license_selector.js
+++ b/app/assets/javascripts/blob/blob_license_selector.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle, padded-blocks */
+/* global Api */
/*= require blob/template_selector */
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js.es6
index adeb8ba1318..268640681d4 100644
--- a/app/assets/javascripts/blob/blob_license_selectors.js.es6
+++ b/app/assets/javascripts/blob/blob_license_selectors.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars, no-param-reassign, padded-blocks */
+/* global BlobLicenseSelector */
+
((global) => {
class BlobLicenseSelectors {
constructor({ $dropdowns, editor }) {
diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6
index 5434a19bcec..7a1ee9998c8 100644
--- a/app/assets/javascripts/blob/template_selector.js.es6
+++ b/app/assets/javascripts/blob/template_selector.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable indent, comma-dangle, object-shorthand, func-names, space-before-function-paren, arrow-parens, no-unused-vars, class-methods-use-this, no-var, consistent-return, prefer-const, no-param-reassign, space-in-parens, max-len */
+
((global) => {
class TemplateSelector {
constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) {
@@ -70,6 +71,8 @@
// e.g.
// Api.gitignoreText item.name, @requestFileSuccess.bind(@)
requestFileSuccess(file, { skipFocus } = {}) {
+ if (!file) return;
+
const oldValue = this.editor.getValue();
let newValue = file.content;
diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
index b801c10f168..8c40e36a80a 100644
--- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, padded-blocks, max-len */
+/* global EditBlob */
+/* global NewCommitForm */
+
/*= require_tree . */
(function() {
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 74cc0af2486..fa43ff611cc 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, padded-blocks, max-len */
+/* global ace */
+/* global BlobGitignoreSelectors */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -60,7 +63,7 @@
content: this.editor.getValue()
}, function(response) {
currentPane.empty().append(response);
- return currentPane.syntaxHighlight();
+ return currentPane.renderGFM();
});
} else {
this.$toggleButton.show();
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index efb22d38513..ab2343c72fc 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable one-var, indent, quote-props, comma-dangle, space-before-function-paren */
+/* global Vue */
+/* global BoardService */
+
//= require vue
//= require vue-resource
//= require Sortable
@@ -22,6 +25,8 @@ $(() => {
gl.IssueBoardsApp.$destroy(true);
}
+ Store.create();
+
gl.IssueBoardsApp = new Vue({
el: $boardApp,
components: {
@@ -37,16 +42,15 @@ $(() => {
issueLinkBase: $boardApp.dataset.issueLinkBase,
detailIssue: Store.detail
},
- init: Store.create.bind(Store),
computed: {
detailIssueVisible () {
return Object.keys(this.detailIssue.issue).length;
- }
+ },
},
created () {
gl.boardService = new BoardService(this.endpoint, this.boardId);
},
- ready () {
+ mounted () {
Store.disabled = this.disabled;
gl.boardService.all()
.then((resp) => {
@@ -60,6 +64,8 @@ $(() => {
}
});
+ this.state.lists = _.sortBy(this.state.lists, 'position');
+
Store.addBlankState();
this.loading = false;
});
@@ -70,6 +76,9 @@ $(() => {
el: '#js-boards-seach',
data: {
filters: Store.state.filters
+ },
+ mounted () {
+ gl.issueBoards.newListDropdownInit();
}
});
});
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
index 0e03d43872b..d1fb0ec48e0 100644
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, one-var, indent, radix */
+/* global Vue */
+/* global Sortable */
+
//= require ./board_blank_state
//= require ./board_delete
//= require ./board_list
@@ -10,6 +13,7 @@
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.Board = Vue.extend({
+ template: '#js-board-template',
components: {
'board-list': gl.issueBoards.BoardList,
'board-delete': gl.issueBoards.BoardDelete,
@@ -24,7 +28,6 @@
return {
detailIssue: Store.detail,
filters: Store.state.filters,
- showIssueForm: false
};
},
watch: {
@@ -58,10 +61,10 @@
},
methods: {
showNewIssueForm() {
- this.showIssueForm = !this.showIssueForm;
+ this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
}
},
- ready () {
+ mounted () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled,
group: 'boards',
@@ -72,13 +75,9 @@
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
const order = this.sortable.toArray(),
- $board = this.$parent.$refs.board[e.oldIndex + 1],
- list = $board.list;
-
- $board.$destroy(true);
+ list = Store.findList('id', parseInt(e.item.dataset.id));
this.$nextTick(() => {
- Store.state.lists.splice(e.newIndex, 0, list);
Store.moveList(list, order);
});
}
@@ -87,8 +86,5 @@
this.sortable = Sortable.create(this.$el.parentNode, options);
},
- beforeDestroy () {
- Store.state.lists.$remove(this.list);
- }
});
})();
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6
index 885553690d3..0a47a22fad2 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.js.es6
+++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, comma-dangle, semi */
+/* global Vue */
+/* global ListLabel */
+
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -30,6 +33,8 @@
});
});
+ Store.state.lists = _.sortBy(Store.state.lists, 'position');
+
// Save the labels
gl.boardService.generateDefaultLists()
.then((resp) => {
diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6
index 2f6c03e3538..5fc50280811 100644
--- a/app/assets/javascripts/boards/components/board_card.js.es6
+++ b/app/assets/javascripts/boards/components/board_card.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */
+/* global Vue */
+
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -6,6 +8,7 @@
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardCard = Vue.extend({
+ template: '#js-board-list-card',
props: {
list: Object,
issue: Object,
@@ -53,10 +56,8 @@
mouseDown () {
this.showDetail = true;
},
- mouseMove () {
- if (this.showDetail) {
- this.showDetail = false;
- }
+ mouseMove() {
+ this.showDetail = false;
},
showIssue (e) {
const targetTagName = e.target.tagName.toLowerCase();
diff --git a/app/assets/javascripts/boards/components/board_delete.js.es6 b/app/assets/javascripts/boards/components/board_delete.js.es6
index c45e1926c5c..861600424a5 100644
--- a/app/assets/javascripts/boards/components/board_delete.js.es6
+++ b/app/assets/javascripts/boards/components/board_delete.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, no-alert */
+/* global Vue */
+
(() => {
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
index 34fc7694241..6711930622b 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, max-len, no-plusplus */
+/* global Vue */
+/* global Sortable */
+
//= require ./board_card
//= require ./board_new_issue
@@ -9,6 +12,7 @@
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardList = Vue.extend({
+ template: '#js-board-list-template',
components: {
'board-card': gl.issueBoards.BoardCard,
'board-new-issue': gl.issueBoards.BoardNewIssue
@@ -19,20 +23,20 @@
issues: Array,
loading: Boolean,
issueLinkBase: String,
- showIssueForm: Boolean
},
data () {
return {
scrollOffset: 250,
filters: Store.state.filters,
- showCount: false
+ showCount: false,
+ showIssueForm: false
};
},
watch: {
filters: {
handler () {
this.list.loadingMore = false;
- this.$els.list.scrollTop = 0;
+ this.$refs.list.scrollTop = 0;
},
deep: true
},
@@ -51,15 +55,20 @@
});
}
},
+ computed: {
+ orderedIssues () {
+ return _.sortBy(this.issues, 'priority');
+ },
+ },
methods: {
listHeight () {
- return this.$els.list.getBoundingClientRect().height;
+ return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight () {
- return this.$els.list.scrollHeight;
+ return this.$refs.list.scrollHeight;
},
scrollTop () {
- return this.$els.list.scrollTop + this.listHeight();
+ return this.$refs.list.scrollTop + this.listHeight();
},
loadNextPage () {
const getIssues = this.list.nextPage();
@@ -72,8 +81,9 @@
}
},
},
- ready () {
+ mounted () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
+ scroll: document.querySelectorAll('.boards-list')[0],
group: 'issues',
sort: false,
disabled: this.disabled,
@@ -81,23 +91,25 @@
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
- Store.moving.issue = card.issue;
+ card.showDetail = false;
Store.moving.list = card.list;
+ Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId);
gl.issueBoards.onStart();
},
onAdd: (e) => {
- gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue);
+ gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex);
+
+ this.$nextTick(() => {
+ e.item.remove();
+ });
},
- onRemove: (e) => {
- this.$refs.issue[e.oldIndex].$destroy(true);
- }
});
- this.sortable = Sortable.create(this.$els.list, options);
+ this.sortable = Sortable.create(this.$refs.list, options);
// Scroll event on list to load more
- this.$els.list.onscroll = () => {
+ this.$refs.list.onscroll = () => {
if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
this.loadNextPage();
}
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6
index 7fc0bfd56f3..2386d3a613c 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.js.es6
+++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, no-unused-vars */
+/* global Vue */
+/* global ListIssue */
+
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -7,7 +10,6 @@
gl.issueBoards.BoardNewIssue = Vue.extend({
props: {
list: Object,
- showIssueForm: Boolean
},
data() {
return {
@@ -15,11 +17,6 @@
error: false
};
},
- watch: {
- showIssueForm () {
- this.$els.input.focus();
- }
- },
methods: {
submit(e) {
e.preventDefault();
@@ -37,28 +34,30 @@
this.list.newIssue(issue)
.then((data) => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
- $(this.$els.submitButton).enable();
+ $(this.$refs.submitButton).enable();
Store.detail.issue = issue;
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
- $(this.$els.submitButton).enable();
+ $(this.$refs.submitButton).enable();
// Remove the issue
this.list.removeIssue(issue);
// Show error message
this.error = true;
- this.showIssueForm = true;
});
this.cancel();
},
cancel() {
- this.showIssueForm = false;
this.title = '';
+ this.$parent.showIssueForm = false;
}
- }
+ },
+ mounted() {
+ this.$refs.input.focus();
+ },
});
})();
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6
index 4928320d015..02459722bbf 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js.es6
+++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6
@@ -1,4 +1,10 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, no-new */
+/* global Vue */
+/* global IssuableContext */
+/* global MilestoneSelect */
+/* global LabelsSelect */
+/* global Sidebar */
+
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -41,13 +47,13 @@
this.detail.issue = {};
}
},
- ready () {
+ mounted () {
new IssuableContext(this.currentUser);
new MilestoneSelect();
new gl.DueDateSelectors();
new LabelsSelect();
new Sidebar();
- new Subscription('.subscription');
+ gl.Subscription.bindAll('.subscription');
}
});
})();
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
index 14f618fd5d5..3f5cf8420a8 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
@@ -1,5 +1,9 @@
-/* eslint-disable */
-$(() => {
+/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var, indent */
+
+(() => {
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
const Store = gl.issueBoards.BoardsStore;
$(document).off('created.label').on('created.label', (e, label) => {
@@ -15,54 +19,58 @@ $(() => {
});
});
- $('.js-new-board-list').each(function () {
- const $this = $(this);
- new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
+ gl.issueBoards.newListDropdownInit = () => {
+ $('.js-new-board-list').each(function () {
+ const $this = $(this);
+ new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
- $this.glDropdown({
- data(term, callback) {
- $.get($this.attr('data-labels'))
- .then((resp) => {
- callback(resp);
- });
- },
- renderRow (label) {
- const active = Store.findList('title', label.title),
- $li = $('<li />'),
- $a = $('<a />', {
- class: (active ? `is-active js-board-list-${active.id}` : ''),
- text: label.title,
- href: '#'
- }),
- $labelColor = $('<span />', {
- class: 'dropdown-label-box',
- style: `background-color: ${label.color}`
- });
+ $this.glDropdown({
+ data(term, callback) {
+ $.get($this.attr('data-labels'))
+ .then((resp) => {
+ callback(resp);
+ });
+ },
+ renderRow (label) {
+ const active = Store.findList('title', label.title),
+ $li = $('<li />'),
+ $a = $('<a />', {
+ class: (active ? `is-active js-board-list-${active.id}` : ''),
+ text: label.title,
+ href: '#'
+ }),
+ $labelColor = $('<span />', {
+ class: 'dropdown-label-box',
+ style: `background-color: ${label.color}`
+ });
- return $li.append($a.prepend($labelColor));
- },
- search: {
- fields: ['title']
- },
- filterable: true,
- selectable: true,
- multiSelect: true,
- clicked (label, $el, e) {
- e.preventDefault();
+ return $li.append($a.prepend($labelColor));
+ },
+ search: {
+ fields: ['title']
+ },
+ filterable: true,
+ selectable: true,
+ multiSelect: true,
+ clicked (label, $el, e) {
+ e.preventDefault();
- if (!Store.findList('title', label.title)) {
- Store.new({
- title: label.title,
- position: Store.state.lists.length - 2,
- list_type: 'label',
- label: {
- id: label.id,
+ if (!Store.findList('title', label.title)) {
+ Store.new({
title: label.title,
- color: label.color
- }
- });
+ position: Store.state.lists.length - 2,
+ list_type: 'label',
+ label: {
+ id: label.id,
+ title: label.title,
+ color: label.color
+ }
+ });
+
+ Store.state.lists = _.sortBy(Store.state.lists, 'position');
+ }
}
- }
+ });
});
- });
-});
+ };
+})();
diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
index 9eceac4eddd..7e192e90fe6 100644
--- a/app/assets/javascripts/boards/filters/due_date_filters.js.es6
+++ b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* global Vue */
+
Vue.filter('due-date', (value) => {
const date = new Date(value);
return $.datepicker.formatDate('M d, yy', date);
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
index db9a5a8e40a..a5e62ed775d 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars, no-mixed-operators, prefer-const, comma-dangle, semi */
+/* global DocumentTouch */
+
((w) => {
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
@@ -23,7 +25,7 @@
fallbackOnBody: true,
ghostClass: 'is-ghost',
filter: '.board-delete, .btn',
- delay: gl.issueBoards.touchEnabled ? 100 : 50,
+ delay: gl.issueBoards.touchEnabled ? 100 : 0,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20,
onStart: gl.issueBoards.onStart,
diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6
index 21d735e8231..1199e022ff1 100644
--- a/app/assets/javascripts/boards/models/issue.js.es6
+++ b/app/assets/javascripts/boards/models/issue.js.es6
@@ -1,4 +1,9 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, space-in-parens, arrow-parens, comma-dangle, max-len */
+/* global Vue */
+/* global ListLabel */
+/* global ListMilestone */
+/* global ListUser */
+
class ListIssue {
constructor (obj) {
this.id = obj.iid;
diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6
index 0910fe9a854..8f20a1bbec7 100644
--- a/app/assets/javascripts/boards/models/label.js.es6
+++ b/app/assets/javascripts/boards/models/label.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars, space-before-function-paren */
+
class ListLabel {
constructor (obj) {
this.id = obj.id;
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
index b331a26fed5..a8d38c16485 100644
--- a/app/assets/javascripts/boards/models/list.js.es6
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-plusplus, prefer-const, space-in-parens, no-shadow, no-param-reassign, max-len, no-unused-vars */
+/* global ListIssue */
+/* global ListLabel */
+
class List {
constructor (obj) {
this.id = obj.id;
@@ -42,7 +45,8 @@ class List {
}
destroy () {
- gl.issueBoards.BoardsStore.state.lists.$remove(this);
+ const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this);
+ gl.issueBoards.BoardsStore.state.lists.splice(index, 1);
gl.issueBoards.BoardsStore.updateNewListDropdown(this.id);
gl.boardService.destroyList(this.id);
@@ -105,9 +109,13 @@ class List {
});
}
- addIssue (issue, listFrom) {
+ addIssue (issue, listFrom, newIndex) {
if (!this.findIssue(issue.id)) {
- this.issues.push(issue);
+ if (newIndex !== undefined) {
+ this.issues.splice(newIndex, 0, issue);
+ } else {
+ this.issues.push(issue);
+ }
if (this.label) {
issue.addLabel(this.label);
diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js.es6
index a48969e19c9..9c173c1b70b 100644
--- a/app/assets/javascripts/boards/models/milestone.js.es6
+++ b/app/assets/javascripts/boards/models/milestone.js.es6
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars */
+
class ListMilestone {
- constructor (obj) {
+ constructor(obj) {
this.id = obj.id;
this.title = obj.title;
}
diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js.es6
index 583a973fc46..a8a3892e2ad 100644
--- a/app/assets/javascripts/boards/models/user.js.es6
+++ b/app/assets/javascripts/boards/models/user.js.es6
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars */
+
class ListUser {
- constructor (user) {
+ constructor(user) {
this.id = user.id;
this.name = user.name;
this.username = user.username;
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
index f59a2ed7937..189a8703198 100644
--- a/app/assets/javascripts/boards/services/board_service.js.es6
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, prefer-const, no-extra-semi, max-len, no-unused-vars */
+/* global Vue */
+
class BoardService {
constructor (root, boardId) {
this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
index 175e034afed..e7a14ea5bca 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js.es6
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, one-var, indent, space-in-parens, no-shadow, radix, dot-notation, semi, max-len */
+/* global Cookies */
+/* global List */
+
(() => {
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
@@ -39,6 +42,8 @@
// Remove any new issues from the backlog
// as they will be visible in the new list
list.issues.forEach(backlogList.removeIssue.bind(backlogList));
+
+ this.state.lists = _.sortBy(this.state.lists, 'position');
});
this.removeBlankState();
},
@@ -58,6 +63,8 @@
title: 'Welcome to your Issue Board!',
position: 0
});
+
+ this.state.lists = _.sortBy(this.state.lists, 'position');
},
removeBlankState () {
this.removeList('blank');
@@ -85,14 +92,14 @@
});
listFrom.update();
},
- moveIssueToList (listFrom, listTo, issue) {
+ moveIssueToList (listFrom, listTo, issue, newIndex) {
const issueTo = listTo.findIssue(issue.id),
issueLists = issue.getLists(),
listLabels = issueLists.map( listIssue => listIssue.label );
// Add to new lists issues if it doesn't already exist
if (!issueTo) {
- listTo.addIssue(issue, listFrom);
+ listTo.addIssue(issue, listFrom, newIndex);
}
if (listTo.type === 'done' && listFrom.type !== 'backlog') {
diff --git a/app/assets/javascripts/boards/test_utils/simulate_drag.js b/app/assets/javascripts/boards/test_utils/simulate_drag.js
index 039ca491cf5..01e09ec482e 100644
--- a/app/assets/javascripts/boards/test_utils/simulate_drag.js
+++ b/app/assets/javascripts/boards/test_utils/simulate_drag.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable wrap-iife, func-names, strict, indent, no-tabs, no-var, vars-on-top, no-param-reassign, object-shorthand, no-shadow, comma-dangle, prefer-template, consistent-return, no-mixed-operators, no-unused-vars, object-curly-spacing, no-unused-expressions, prefer-arrow-callback, max-len */
(function () {
'use strict';
diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
index 80f137ca12e..3723a2039f9 100644
--- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
+++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars, no-plusplus */
+/* global Vue */
+
Vue.http.interceptors.push((request, next) => {
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js
index 5d4d23e26c6..a7e72430141 100644
--- a/app/assets/javascripts/breakpoints.js
+++ b/app/assets/javascripts/breakpoints.js
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, padded-blocks, no-return-assign, new-parens, no-param-reassign, max-len */
+
(function() {
- this.Breakpoints = (function() {
+ var Breakpoints = (function() {
var BreakpointInstance, instance;
function Breakpoints() {}
@@ -68,4 +69,5 @@
};
})(this));
+ window.Breakpoints = Breakpoints;
}).call(this);
diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js
index 576f4c76c1e..30432dae278 100644
--- a/app/assets/javascripts/broadcast_message.js
+++ b/app/assets/javascripts/broadcast_message.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, no-else-return, object-shorthand, comma-dangle, padded-blocks, max-len */
(function() {
$(function() {
var previewPath;
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 12e653f4122..824febe3fd3 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, semi, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top, padded-blocks */
+/* global Breakpoints */
+/* global Turbolinks */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -8,56 +11,55 @@
Build.state = null;
function Build(options) {
- this.page_url = options.page_url;
- this.build_url = options.build_url;
- this.build_status = options.build_status;
- this.state = options.state1;
- this.build_stage = options.build_stage;
- this.hideSidebar = bind(this.hideSidebar, this);
- this.toggleSidebar = bind(this.toggleSidebar, this);
+ options = options || $('.js-build-options').data();
+ this.pageUrl = options.pageUrl;
+ this.buildUrl = options.buildUrl;
+ this.buildStatus = options.buildStatus;
+ this.state = options.logState;
+ this.buildStage = options.buildStage;
this.updateDropdown = bind(this.updateDropdown, this);
this.$document = $(document);
clearInterval(Build.interval);
// Init breakpoint checker
this.bp = Breakpoints.get();
+
this.initSidebar();
+ this.$buildScroll = $('#js-build-scroll');
- this.populateJobs(this.build_stage);
- this.updateStageDropdownText(this.build_stage);
+ this.populateJobs(this.buildStage);
+ this.updateStageDropdownText(this.buildStage);
+ this.sidebarOnResize();
- $(window).off('resize.build').on('resize.build', this.hideSidebar);
+ this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
- $('#js-build-scroll > a').off('click').on('click', this.stepTrace);
+ $(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this));
+ $('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace);
this.updateArtifactRemoveDate();
if ($('#build-trace').length) {
this.getInitialBuildTrace();
- this.initScrollButtons();
+ this.initScrollButtonAffix();
}
- if (this.build_status === "running" || this.build_status === "pending") {
+ if (this.buildStatus === "running" || this.buildStatus === "pending") {
+ // Bind autoscroll button to follow build output
$('#autoscroll-button').on('click', function() {
var state;
state = $(this).data("state");
if ("enabled" === state) {
$(this).data("state", "disabled");
- return $(this).text("enable autoscroll");
+ return $(this).text("Enable autoscroll");
} else {
$(this).data("state", "enabled");
- return $(this).text("disable autoscroll");
+ return $(this).text("Disable autoscroll");
}
- //
- // Bind autoscroll button to follow build output
- //
});
Build.interval = setInterval((function(_this) {
+ // Check for new build output if user still watching build page
+ // Only valid for runnig build when output changes during time
return function() {
- if (window.location.href.split("#").first() === _this.page_url) {
+ if (_this.location() === _this.pageUrl) {
return _this.getBuildTrace();
}
};
- //
- // Check for new build output if user still watching build page
- // Only valid for runnig build when output changes during time
- //
})(this), 4000);
}
}
@@ -72,20 +74,23 @@
top: this.sidebarTranslationLimits.max
});
this.$sidebar.niceScroll();
- this.hideSidebar();
this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
this.$document.off('scroll.translateSidebar').on('scroll.translateSidebar', this.translateSidebar.bind(this));
};
+ Build.prototype.location = function() {
+ return window.location.href.split("#")[0];
+ };
+
Build.prototype.getInitialBuildTrace = function() {
var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']
return $.ajax({
- url: this.build_url,
+ url: this.buildUrl,
dataType: 'json',
- success: function(build_data) {
- $('.js-build-output').html(build_data.trace_html);
- if (removeRefreshStatuses.indexOf(build_data.status) >= 0) {
+ success: function(buildData) {
+ $('.js-build-output').html(buildData.trace_html);
+ if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
return $('.js-build-refresh').remove();
}
}
@@ -94,7 +99,7 @@
Build.prototype.getBuildTrace = function() {
return $.ajax({
- url: this.page_url + "/trace.json?state=" + (encodeURIComponent(this.state)),
+ url: this.pageUrl + "/trace.json?state=" + (encodeURIComponent(this.state)),
dataType: "json",
success: (function(_this) {
return function(log) {
@@ -108,8 +113,8 @@
$('.js-build-output').html(log.html);
}
return _this.checkAutoscroll();
- } else if (log.status !== _this.build_status) {
- return Turbolinks.visit(_this.page_url);
+ } else if (log.status !== _this.buildStatus) {
+ return Turbolinks.visit(_this.pageUrl);
}
};
})(this)
@@ -122,12 +127,11 @@
}
};
- Build.prototype.initScrollButtons = function() {
- var $body, $buildScroll, $buildTrace;
- $buildScroll = $('#js-build-scroll');
+ Build.prototype.initScrollButtonAffix = function() {
+ var $body, $buildTrace;
$body = $('body');
$buildTrace = $('#build-trace');
- return $buildScroll.affix({
+ return this.$buildScroll.affix({
offset: {
bottom: function() {
return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top);
@@ -136,18 +140,12 @@
});
};
- Build.prototype.shouldHideSidebar = function() {
+ Build.prototype.shouldHideSidebarForViewport = function() {
var bootstrapBreakpoint;
bootstrapBreakpoint = this.bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
};
- Build.prototype.toggleSidebar = function() {
- if (this.shouldHideSidebar()) {
- return this.$sidebar.toggleClass('right-sidebar-expanded right-sidebar-collapsed');
- }
- };
-
Build.prototype.translateSidebar = function(e) {
var newPosition = this.sidebarTranslationLimits.max - (document.body.scrollTop || document.documentElement.scrollTop);
if (newPosition < this.sidebarTranslationLimits.min) newPosition = this.sidebarTranslationLimits.min;
@@ -156,12 +154,20 @@
});
};
- Build.prototype.hideSidebar = function() {
- if (this.shouldHideSidebar()) {
- return this.$sidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
- } else {
- return this.$sidebar.removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
- }
+ Build.prototype.toggleSidebar = function(shouldHide) {
+ var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
+ this.$buildScroll.toggleClass('sidebar-expanded', shouldShow)
+ .toggleClass('sidebar-collapsed', shouldHide);
+ this.$sidebar.toggleClass('right-sidebar-expanded', shouldShow)
+ .toggleClass('right-sidebar-collapsed', shouldHide);
+ };
+
+ Build.prototype.sidebarOnResize = function() {
+ this.toggleSidebar(this.shouldHideSidebarForViewport());
+ };
+
+ Build.prototype.sidebarOnClick = function() {
+ if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
};
Build.prototype.updateArtifactRemoveDate = function() {
@@ -169,7 +175,7 @@
$date = $('.js-artifacts-remove');
if ($date.length) {
date = $date.text();
- return $date.text($.timefor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
+ return $date.text(gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
}
};
@@ -190,6 +196,7 @@
};
Build.prototype.stepTrace = function(e) {
+ var $currentTarget;
e.preventDefault();
$currentTarget = $(e.currentTarget);
$.scrollTo($currentTarget.attr('href'), {
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
index 49f84581650..c423a548a30 100644
--- a/app/assets/javascripts/build_artifacts.js
+++ b/app/assets/javascripts/build_artifacts.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, padded-blocks, max-len */
(function() {
this.BuildArtifacts = (function() {
function BuildArtifacts() {
diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js.es6
index 0ecd20bc11e..993424d422f 100644
--- a/app/assets/javascripts/build_variables.js.es6
+++ b/app/assets/javascripts/build_variables.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, prefer-arrow-callback, space-before-blocks, space-before-function-paren, comma-spacing, max-len */
+
$(function(){
$('.reveal-variables').off('click').on('click',function(){
$('.js-build').toggle().niceScroll();
diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js
index fac5b4f17da..67b33a4d7ee 100644
--- a/app/assets/javascripts/commit.js
+++ b/app/assets/javascripts/commit.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, padded-blocks */
+/* global CommitFile */
+
(function() {
this.Commit = (function() {
function Commit() {
diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js
index 16d63729d31..27512312c7c 100644
--- a/app/assets/javascripts/commit/file.js
+++ b/app/assets/javascripts/commit/file.js
@@ -1,9 +1,11 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, padded-blocks */
+/* global ImageFile */
+
(function() {
this.CommitFile = (function() {
function CommitFile(file) {
if ($('.image', file).length) {
- new ImageFile(file);
+ new gl.ImageFile(file);
}
}
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index ffddce1297b..fd8910e916f 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -1,6 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, padded-blocks, max-len */
(function() {
- this.ImageFile = (function() {
+ gl.ImageFile = (function() {
var prepareFrames;
// Width where images must fits in, for 2-up this gets divided by 2
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index c765d233831..24a6e4ff0e9 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, padded-blocks, max-len, prefer-arrow-callback */
+/* global Pager */
+
(function() {
this.CommitsList = (function() {
function CommitsList() {}
@@ -6,14 +8,16 @@
CommitsList.timer = null;
CommitsList.init = function(limit) {
- $("body").on("click", ".day-commits-table li.commit", function(event) {
- if (event.target.nodeName !== "A") {
+ $("body").on("click", ".day-commits-table li.commit", function(e) {
+ if (e.target.nodeName !== "A") {
location.href = $(this).attr("url");
e.stopPropagation();
return false;
}
});
- Pager.init(limit, false);
+ Pager.init(limit, false, false, function() {
+ gl.utils.localTimeAgo($('.js-timeago'));
+ });
this.content = $("#commits-list");
this.searchField = $("#commits-search");
return this.initSearch();
diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js
index b3f769d4129..d4243baadb5 100644
--- a/app/assets/javascripts/compare.js
+++ b/app/assets/javascripts/compare.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, padded-blocks, max-len */
(function() {
this.Compare = (function() {
function Compare(opts) {
@@ -80,7 +80,8 @@
success: function(html) {
loading.hide();
$target.html(html);
- return $('.js-timeago', $target).timeago();
+ var className = '.' + $target[0].className.replace(' ', '.');
+ gl.utils.localTimeAgo($('.js-timeago', className));
}
});
};
diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js.es6
index bd980f87e72..45c974b2b68 100644
--- a/app/assets/javascripts/compare_autocomplete.js.es6
+++ b/app/assets/javascripts/compare_autocomplete.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, no-dupe-keys, wrap-iife, padded-blocks, max-len */
+
(function() {
this.CompareAutocomplete = (function() {
function CompareAutocomplete() {
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
index 143d21adb37..686a48486f3 100644
--- a/app/assets/javascripts/confirm_danger_modal.js
+++ b/app/assets/javascripts/confirm_danger_modal.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, padded-blocks, max-len */
(function() {
this.ConfirmDangerModal = (function() {
function ConfirmDangerModal(form, text) {
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index 7808d7fe313..6a13f38588d 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, padded-blocks, max-len */
+/* global Clipboard */
/*= require clipboard */
@@ -6,7 +7,7 @@
var genericError, genericSuccess, showTooltip;
genericSuccess = function(e) {
- showTooltip(e.trigger, 'Copied!');
+ showTooltip(e.trigger, 'Copied');
// Clear the selection and blur the trigger so it loses its border
e.clearSelection();
return $(e.trigger).blur();
@@ -31,7 +32,7 @@
var originalTitle = $target.data('original-title');
$target
- .attr('title', 'Copied!')
+ .attr('title', 'Copied')
.tooltip('fixTitle')
.tooltip('show')
.attr('title', originalTitle)
diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6
index 744aa0afa03..947c129d5b5 100644
--- a/app/assets/javascripts/create_label.js.es6
+++ b/app/assets/javascripts/create_label.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */
+/* global Api */
+
(function (w) {
class CreateLabelDropdown {
constructor ($el, namespacePath, projectPath) {
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6
new file mode 100644
index 00000000000..b83a4c63fad
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6
@@ -0,0 +1,45 @@
+/* eslint-disable no-param-reassign */
+/* global Vue */
+
+((global) => {
+ global.cycleAnalytics = global.cycleAnalytics || {};
+
+ global.cycleAnalytics.StageCodeComponent = Vue.extend({
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ template: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ </div>
+ <ul class="stage-event-list">
+ <li v-for="mergeRequest in items" class="stage-event-item">
+ <div class="item-details">
+ <img class="avatar" :src="mergeRequest.author.avatarUrl">
+ <h5 class="item-title merge-merquest-title">
+ <a :href="mergeRequest.url">
+ {{ mergeRequest.title }}
+ </a>
+ </h5>
+ <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
+ &middot;
+ <span>
+ Opened
+ <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
+ </span>
+ <span>
+ by
+ <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="mergeRequest.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6
new file mode 100644
index 00000000000..cb1687dcc7a
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6
@@ -0,0 +1,47 @@
+/* eslint-disable no-param-reassign */
+/* global Vue */
+
+((global) => {
+ global.cycleAnalytics = global.cycleAnalytics || {};
+
+ global.cycleAnalytics.StageIssueComponent = Vue.extend({
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ template: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ </div>
+ <ul class="stage-event-list">
+ <li v-for="issue in items" class="stage-event-item">
+ <div class="item-details">
+ <img class="avatar" :src="issue.author.avatarUrl">
+ <h5 class="item-title issue-title">
+ <a class="issue-title" :href="issue.url">
+ {{ issue.title }}
+ </a>
+ </h5>
+ <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
+ &middot;
+ <span>
+ Opened
+ <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
+ </span>
+ <span>
+ by
+ <a :href="issue.author.webUrl" class="issue-author-link">
+ {{ issue.author.name }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="issue.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})(window.gl || (window.gl = {}));
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
new file mode 100644
index 00000000000..513298ba4e7
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6
@@ -0,0 +1,44 @@
+/* eslint-disable no-param-reassign */
+/* global Vue */
+
+((global) => {
+ global.cycleAnalytics = global.cycleAnalytics || {};
+
+ global.cycleAnalytics.StagePlanComponent = Vue.extend({
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ template: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ </div>
+ <ul class="stage-event-list">
+ <li v-for="commit in items" class="stage-event-item">
+ <div class="item-details item-conmmit-component">
+ <img class="avatar" :src="commit.author.avatarUrl">
+ <h5 class="item-title commit-title">
+ <a :href="commit.commitUrl">
+ {{ commit.title }}
+ </a>
+ </h5>
+ <span>
+ First
+ <span class="commit-icon">${global.cycleAnalytics.svgs.iconCommit}</span>
+ <a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a>
+ pushed by
+ <a :href="commit.author.webUrl" class="commit-author-link">
+ {{ commit.author.name }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="commit.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6
new file mode 100644
index 00000000000..73f4205b578
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6
@@ -0,0 +1,47 @@
+/* eslint-disable no-param-reassign */
+/* global Vue */
+
+((global) => {
+ global.cycleAnalytics = global.cycleAnalytics || {};
+
+ global.cycleAnalytics.StageProductionComponent = Vue.extend({
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ template: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ </div>
+ <ul class="stage-event-list">
+ <li v-for="issue in items" class="stage-event-item">
+ <div class="item-details">
+ <img class="avatar" :src="issue.author.avatarUrl">
+ <h5 class="item-title issue-title">
+ <a class="issue-title" :href="issue.url">
+ {{ issue.title }}
+ </a>
+ </h5>
+ <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
+ &middot;
+ <span>
+ Opened
+ <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
+ </span>
+ <span>
+ by
+ <a :href="issue.author.webUrl" class="issue-author-link">
+ {{ issue.author.name }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="issue.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6
new file mode 100644
index 00000000000..501ffb1fac9
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6
@@ -0,0 +1,57 @@
+/* eslint-disable no-param-reassign */
+/* global Vue */
+
+((global) => {
+ global.cycleAnalytics = global.cycleAnalytics || {};
+
+ global.cycleAnalytics.StageReviewComponent = Vue.extend({
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ template: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ </div>
+ <ul class="stage-event-list">
+ <li v-for="mergeRequest in items" class="stage-event-item">
+ <div class="item-details">
+ <img class="avatar" :src="mergeRequest.author.avatarUrl">
+ <h5 class="item-title merge-merquest-title">
+ <a :href="mergeRequest.url">
+ {{ mergeRequest.title }}
+ </a>
+ </h5>
+ <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
+ &middot;
+ <span>
+ Opened
+ <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
+ </span>
+ <span>
+ by
+ <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
+ </span>
+ <template v-if="mergeRequest.state === 'closed'">
+ <span class="merge-request-state">
+ <i class="fa fa-ban"></i>
+ {{ mergeRequest.state.toUpperCase() }}
+ </span>
+ </template>
+ <template v-else>
+ <span class="merge-request-branch" v-if="mergeRequest.branch">
+ <i class= "fa fa-code-fork"></i>
+ <a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
+ </span>
+ </template>
+ </div>
+ <div class="item-time">
+ <total-time :time="mergeRequest.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6
new file mode 100644
index 00000000000..82622232f64
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6
@@ -0,0 +1,44 @@
+/* eslint-disable no-param-reassign */
+/* global Vue */
+
+((global) => {
+ global.cycleAnalytics = global.cycleAnalytics || {};
+
+ global.cycleAnalytics.StageStagingComponent = Vue.extend({
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ template: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ </div>
+ <ul class="stage-event-list">
+ <li v-for="build in items" class="stage-event-item item-build-component">
+ <div class="item-details">
+ <img class="avatar" :src="build.author.avatarUrl">
+ <h5 class="item-title">
+ <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
+ <i class="fa fa-code-fork"></i>
+ <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
+ <span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span>
+ <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
+ </h5>
+ <span>
+ <a :href="build.url" class="build-date">{{ build.date }}</a>
+ by
+ <a :href="build.author.webUrl" class="issue-author-link">
+ {{ build.author.name }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="build.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6
new file mode 100644
index 00000000000..4bfd363a1f1
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6
@@ -0,0 +1,44 @@
+/* eslint-disable no-param-reassign */
+/* global Vue */
+
+((global) => {
+ global.cycleAnalytics = global.cycleAnalytics || {};
+
+ global.cycleAnalytics.StageTestComponent = Vue.extend({
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ template: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ </div>
+ <ul class="stage-event-list">
+ <li v-for="build in items" class="stage-event-item item-build-component">
+ <div class="item-details">
+ <h5 class="item-title">
+ <span class="icon-build-status">${global.cycleAnalytics.svgs.iconBuildStatus}</span>
+ <a :href="build.url" class="item-build-name">{{ build.name }}</a>
+ &middot;
+ <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
+ <i class="fa fa-code-fork"></i>
+ <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
+ <span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span>
+ <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
+ </h5>
+ <span>
+ <a :href="build.url" class="issue-date">
+ {{ build.date }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="build.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6
new file mode 100644
index 00000000000..0d85e1a4678
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6
@@ -0,0 +1,25 @@
+/* eslint-disable no-param-reassign */
+/* global Vue */
+
+((global) => {
+ global.cycleAnalytics = global.cycleAnalytics || {};
+
+ global.cycleAnalytics.TotalTimeComponent = Vue.extend({
+ props: {
+ time: Object,
+ },
+ template: `
+ <span class="total-time">
+ <template v-if="Object.keys(time).length">
+ <template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template>
+ <template v-if="time.hours">{{ time.hours }} <span>hr</span></template>
+ <template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template>
+ <template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template>
+ </template>
+ <template v-else>
+ --
+ </template>
+ </span>
+ `,
+ });
+})(window.gl || (window.gl = {}));
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 331f0209888..2f810a69758 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
@@ -1,98 +1,125 @@
-/* eslint-disable */
-//= require vue
-
-((global) => {
-
- const COOKIE_NAME = 'cycle_analytics_help_dismissed';
- const store = gl.cycleAnalyticsStore = {
- isLoading: true,
- hasError: false,
- isHelpDismissed: Cookies.get(COOKIE_NAME),
- analytics: {}
- };
+/* global Vue */
+/* global Cookies */
+/* global Flash */
- gl.CycleAnalytics = class CycleAnalytics {
- constructor() {
- const that = this;
-
- this.vue = new Vue({
- el: '#cycle-analytics',
- name: 'CycleAnalytics',
- created: this.fetchData(),
- data: store,
- methods: {
- dismissLanding() {
- that.dismissLanding();
- }
- }
- });
- }
-
- fetchData(options) {
- store.isLoading = true;
- options = options || { startDate: 30 };
-
- $.ajax({
- url: $('#cycle-analytics').data('request-path'),
- method: 'GET',
- dataType: 'json',
- contentType: 'application/json',
- data: {
- cycle_analytics: {
- start_date: options.startDate
- }
+//= require vue
+//= require_tree ./svg
+//= require_tree .
+
+$(() => {
+ const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
+ const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
+ const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore;
+ const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({
+ requestPath: cycleAnalyticsEl.dataset.requestPath,
+ });
+
+ gl.cycleAnalyticsApp = new Vue({
+ el: '#cycle-analytics',
+ name: 'CycleAnalytics',
+ data: {
+ state: cycleAnalyticsStore.state,
+ isLoading: false,
+ isLoadingStage: false,
+ isEmptyStage: false,
+ hasError: false,
+ startDate: 30,
+ isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
+ },
+ computed: {
+ currentStage() {
+ return cycleAnalyticsStore.currentActiveStage();
+ },
+ },
+ components: {
+ 'stage-issue-component': gl.cycleAnalytics.StageIssueComponent,
+ 'stage-plan-component': gl.cycleAnalytics.StagePlanComponent,
+ 'stage-code-component': gl.cycleAnalytics.StageCodeComponent,
+ 'stage-test-component': gl.cycleAnalytics.StageTestComponent,
+ 'stage-review-component': gl.cycleAnalytics.StageReviewComponent,
+ 'stage-staging-component': gl.cycleAnalytics.StageStagingComponent,
+ 'stage-production-component': gl.cycleAnalytics.StageProductionComponent,
+ },
+ created() {
+ this.fetchCycleAnalyticsData();
+ },
+ methods: {
+ handleError() {
+ cycleAnalyticsStore.setErrorState(true);
+ return new Flash('There was an error while fetching cycle analytics data.');
+ },
+ initDropdown() {
+ const $dropdown = $('.js-ca-dropdown');
+ const $label = $dropdown.find('.dropdown-label');
+
+ $dropdown.find('li a').off('click').on('click', (e) => {
+ e.preventDefault();
+ const $target = $(e.currentTarget);
+ this.startDate = $target.data('value');
+
+ $label.text($target.text().trim());
+ this.fetchCycleAnalyticsData({ startDate: this.startDate });
+ });
+ },
+ fetchCycleAnalyticsData(options) {
+ const fetchOptions = options || { startDate: this.startDate };
+
+ this.isLoading = true;
+
+ cycleAnalyticsService
+ .fetchCycleAnalyticsData(fetchOptions)
+ .done((response) => {
+ cycleAnalyticsStore.setCycleAnalyticsData(response);
+ this.selectDefaultStage();
+ this.initDropdown();
+ })
+ .error(() => {
+ this.handleError();
+ })
+ .always(() => {
+ this.isLoading = false;
+ });
+ },
+ selectDefaultStage() {
+ const stage = this.state.stages.first();
+ this.selectStage(stage);
+ },
+ selectStage(stage) {
+ if (this.isLoadingStage) return;
+ if (this.currentStage === stage) return;
+
+ if (!stage.isUserAllowed) {
+ cycleAnalyticsStore.setActiveStage(stage);
+ return;
}
- }).done((data) => {
- this.decorateData(data);
- this.initDropdown();
- })
- .error((data) => {
- this.handleError(data);
- })
- .always(() => {
- store.isLoading = false;
- })
- }
-
- decorateData(data) {
- data.summary = data.summary || [];
- data.stats = data.stats || [];
-
- data.summary.forEach((item) => {
- item.value = item.value || '-';
- });
-
- data.stats.forEach((item) => {
- item.value = item.value || '- - -';
- });
-
- store.analytics = data;
- }
-
- handleError(data) {
- store.hasError = true;
- new Flash('There was an error while fetching cycle analytics data.', 'alert');
- }
-
- dismissLanding() {
- store.isHelpDismissed = true;
- Cookies.set(COOKIE_NAME, true);
- }
-
- initDropdown() {
- const $dropdown = $('.js-ca-dropdown');
- const $label = $dropdown.find('.dropdown-label');
-
- $dropdown.find('li a').off('click').on('click', (e) => {
- e.preventDefault();
- const $target = $(e.currentTarget);
- const value = $target.data('value');
-
- $label.text($target.text().trim());
- this.fetchData({ startDate: value });
- })
- }
-
- }
-})(window.gl || (window.gl = {}));
+ this.isLoadingStage = true;
+ cycleAnalyticsStore.setStageEvents([]);
+ cycleAnalyticsStore.setActiveStage(stage);
+
+ cycleAnalyticsService
+ .fetchStageData({
+ stage,
+ startDate: this.startDate,
+ })
+ .done((response) => {
+ this.isEmptyStage = !response.events.length;
+ cycleAnalyticsStore.setStageEvents(response.events);
+ })
+ .error(() => {
+ this.isEmptyStage = true;
+ })
+ .always(() => {
+ this.isLoadingStage = false;
+ });
+ },
+ dismissOverviewDialog() {
+ this.isOverviewDialogDismissed = true;
+ Cookies.set(OVERVIEW_DIALOG_COOKIE, '1');
+ },
+ },
+ });
+
+ // Register global components
+ Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent);
+});
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6
new file mode 100644
index 00000000000..9f74b14c4b9
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6
@@ -0,0 +1,41 @@
+/* eslint-disable no-param-reassign */
+((global) => {
+ global.cycleAnalytics = global.cycleAnalytics || {};
+
+ class CycleAnalyticsService {
+ constructor(options) {
+ this.requestPath = options.requestPath;
+ }
+
+ fetchCycleAnalyticsData(options) {
+ options = options || { startDate: 30 };
+
+ return $.ajax({
+ url: this.requestPath,
+ method: 'GET',
+ dataType: 'json',
+ contentType: 'application/json',
+ data: {
+ cycle_analytics: {
+ start_date: options.startDate,
+ },
+ },
+ });
+ }
+
+ fetchStageData(options) {
+ const {
+ stage,
+ startDate,
+ } = options;
+
+ return $.get(`${this.requestPath}/events/${stage.title.toLowerCase()}.json`, {
+ cycle_analytics: {
+ start_date: startDate,
+ },
+ });
+ }
+ }
+
+ global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6
new file mode 100644
index 00000000000..be732971c7f
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6
@@ -0,0 +1,94 @@
+/* eslint-disable no-param-reassign */
+((global) => {
+ global.cycleAnalytics = global.cycleAnalytics || {};
+
+ const EMPTY_STAGE_TEXTS = {
+ issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
+ plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
+ code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
+ test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
+ review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
+ staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
+ production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.',
+ };
+
+ global.cycleAnalytics.CycleAnalyticsStore = {
+ state: {
+ summary: '',
+ stats: '',
+ analytics: '',
+ events: [],
+ stages: [],
+ },
+ setCycleAnalyticsData(data) {
+ this.state = Object.assign(this.state, this.decorateData(data));
+ },
+ decorateData(data) {
+ const newData = {};
+
+ newData.stages = data.stats || [];
+ newData.summary = data.summary || [];
+
+ newData.summary.forEach((item) => {
+ item.value = item.value || '-';
+ });
+
+ newData.stages.forEach((item) => {
+ const stageName = item.title.toLowerCase();
+ item.active = false;
+ item.isUserAllowed = data.permissions[stageName];
+ item.emptyStageText = EMPTY_STAGE_TEXTS[stageName];
+ item.component = `stage-${stageName}-component`;
+ });
+ newData.analytics = data;
+ return newData;
+ },
+ setLoadingState(state) {
+ this.state.isLoading = state;
+ },
+ setErrorState(state) {
+ this.state.hasError = state;
+ },
+ deactivateAllStages() {
+ this.state.stages.forEach((stage) => {
+ stage.active = false;
+ });
+ },
+ setActiveStage(stage) {
+ this.deactivateAllStages();
+ stage.active = true;
+ },
+ setStageEvents(events) {
+ this.state.events = this.decorateEvents(events);
+ },
+ decorateEvents(events) {
+ const newEvents = [];
+
+ events.forEach((item) => {
+ if (!item) return;
+
+ item.totalTime = item.total_time;
+ item.author.webUrl = item.author.web_url;
+ item.author.avatarUrl = item.author.avatar_url;
+
+ if (item.created_at) item.createdAt = item.created_at;
+ if (item.short_sha) item.shortSha = item.short_sha;
+ if (item.commit_url) item.commitUrl = item.commit_url;
+
+ delete item.author.web_url;
+ delete item.author.avatar_url;
+ delete item.total_time;
+ delete item.created_at;
+ delete item.short_sha;
+ delete item.commit_url;
+
+ newEvents.push(item);
+ });
+
+ return newEvents;
+ },
+ currentActiveStage() {
+ return this.state.stages.find(stage => stage.active);
+ },
+ };
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6
new file mode 100644
index 00000000000..5d486bcaf66
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6
@@ -0,0 +1,7 @@
+/* eslint-disable no-param-reassign */
+((global) => {
+ global.cycleAnalytics = global.cycleAnalytics || {};
+ global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
+
+ global.cycleAnalytics.svgs.iconBranch = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><path fill="#8C8C8C" fill-rule="evenodd" d="M9.678 6.722C9.353 5.167 8.053 4 6.5 4S3.647 5.167 3.322 6.722h-2.6c-.397 0-.722.35-.722.778 0 .428.325.778.722.778h2.6C3.647 9.833 4.947 11 6.5 11s2.853-1.167 3.178-2.722h2.6c.397 0 .722-.35.722-.778 0-.428-.325-.778-.722-.778h-2.6zM4.694 7.5c0-1.09.795-1.944 1.806-1.944 1.01 0 1.806.855 1.806 1.944 0 1.09-.795 1.944-1.806 1.944-1.01 0-1.806-.855-1.806-1.944z"/></svg>';
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6
new file mode 100644
index 00000000000..661bf9e9f1c
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6
@@ -0,0 +1,7 @@
+/* eslint-disable no-param-reassign */
+((global) => {
+ global.cycleAnalytics = global.cycleAnalytics || {};
+ global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
+
+ global.cycleAnalytics.svgs.iconBuildStatus = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><g fill="#31AF64" fill-rule="evenodd"><path d="M12.5 7c0-3.038-2.462-5.5-5.5-5.5S1.5 3.962 1.5 7s2.462 5.5 5.5 5.5 5.5-2.462 5.5-5.5zM0 7c0-3.866 3.134-7 7-7s7 3.134 7 7-3.134 7-7 7-7-3.134-7-7z"/><path d="M6.28 7.697L5.045 6.464c-.117-.117-.305-.117-.42-.002l-.614.614c-.11.113-.11.303.007.42l1.91 1.91c.19.19.51.197.703.004l.264-.265L9.997 6.04c.108-.107.107-.293-.01-.408l-.612-.614c-.114-.113-.298-.12-.41-.01L6.28 7.7z"/></g></svg>';
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6
new file mode 100644
index 00000000000..2208c27a619
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6
@@ -0,0 +1,7 @@
+/* eslint-disable no-param-reassign */
+((global) => {
+ global.cycleAnalytics = global.cycleAnalytics || {};
+ global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
+
+ global.cycleAnalytics.svgs.iconCommit = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path fill="#8F8F8F" fill-rule="evenodd" d="M28.777 18c-.91-4.008-4.494-7-8.777-7-4.283 0-7.868 2.992-8.777 7H4.01C2.9 18 2 18.895 2 20c0 1.112.9 2 2.01 2h7.213c.91 4.008 4.494 7 8.777 7 4.283 0 7.868-2.992 8.777-7h7.214C37.1 22 38 21.105 38 20c0-1.112-.9-2-2.01-2h-7.213zM20 25c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5z"/></svg>';
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
deleted file mode 100644
index 4ddafff428f..00000000000
--- a/app/assets/javascripts/diff.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/* eslint-disable */
-(function() {
- this.Diff = (function() {
- var UNFOLD_COUNT;
-
- UNFOLD_COUNT = 20;
-
- function Diff() {
- $('.files .diff-file').singleFileDiff();
- this.filesCommentButton = $('.files .diff-file').filesCommentButton();
- if (this.diffViewType() === 'parallel') {
- $('.content-wrapper .container-fluid').removeClass('container-limited');
- }
- $(document).off('click', '.js-unfold');
- $(document).on('click', '.js-unfold', (function(_this) {
- return function(event) {
- var line_number, link, file, offset, old_line, params, prev_new_line, prev_old_line, ref, ref1, since, target, to, unfold, unfoldBottom;
- target = $(event.target);
- unfoldBottom = target.hasClass('js-unfold-bottom');
- unfold = true;
- ref = _this.lineNumbers(target.parent()), old_line = ref[0], line_number = ref[1];
- offset = line_number - old_line;
- if (unfoldBottom) {
- line_number += 1;
- since = line_number;
- to = line_number + UNFOLD_COUNT;
- } else {
- ref1 = _this.lineNumbers(target.parent().prev()), prev_old_line = ref1[0], prev_new_line = ref1[1];
- line_number -= 1;
- to = line_number;
- if (line_number - UNFOLD_COUNT > prev_new_line + 1) {
- since = line_number - UNFOLD_COUNT;
- } else {
- since = prev_new_line + 1;
- unfold = false;
- }
- }
- file = target.parents('.diff-file');
- link = file.data('blob-diff-path');
- params = {
- since: since,
- to: to,
- bottom: unfoldBottom,
- offset: offset,
- unfold: unfold,
- // indent is used to compensate for single space indent to fit
- // '+' and '-' prepended to diff lines,
- // see https://gitlab.com/gitlab-org/gitlab-ce/issues/707
- indent: 1,
- view: file.data('view')
- };
- return $.get(link, params, function(response) {
- return target.parent().replaceWith(response);
- });
- };
- })(this));
- }
-
- Diff.prototype.diffViewType = function() {
- return $('.inline-parallel-buttons a.active').data('view-type');
- }
-
- Diff.prototype.lineNumbers = function(line) {
- if (!line.children().length) {
- return [0, 0];
- }
-
- return line.find('.diff-line-num').map(function() {
- return parseInt($(this).data('linenumber'));
- });
- };
-
- return Diff;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6
new file mode 100644
index 00000000000..9cf33e62958
--- /dev/null
+++ b/app/assets/javascripts/diff.js.es6
@@ -0,0 +1,112 @@
+/* eslint-disable class-methods-use-this */
+
+(() => {
+ const UNFOLD_COUNT = 20;
+
+ class Diff {
+ constructor() {
+ const $diffFile = $('.files .diff-file');
+ $diffFile.singleFileDiff();
+ $diffFile.filesCommentButton();
+
+ $diffFile.each((index, file) => new gl.ImageFile(file));
+
+ if (this.diffViewType() === 'parallel') {
+ $('.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));
+
+ this.highlighSelectedLine();
+ }
+
+ handleClickUnfold(e) {
+ const $target = $(e.target);
+ // current babel config relies on iterators implementation, so we cannot simply do:
+ // const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent());
+ const ref = this.lineNumbers($target.parent());
+ const oldLineNumber = ref[0];
+ const newLineNumber = ref[1];
+ const offset = newLineNumber - oldLineNumber;
+ const bottom = $target.hasClass('js-unfold-bottom');
+ let since;
+ let to;
+ let unfold = true;
+
+ if (bottom) {
+ const lineNumber = newLineNumber + 1;
+ since = lineNumber;
+ to = lineNumber + UNFOLD_COUNT;
+ } else {
+ const lineNumber = newLineNumber - 1;
+ since = lineNumber - UNFOLD_COUNT;
+ to = lineNumber;
+
+ // make sure we aren't loading more than we need
+ const prevNewLine = this.lineNumbers($target.parent().prev())[1];
+ if (since <= prevNewLine + 1) {
+ since = prevNewLine + 1;
+ unfold = false;
+ }
+ }
+
+ const file = $target.parents('.diff-file');
+ const link = file.data('blob-diff-path');
+ const view = file.data('view');
+
+ const params = { since, to, bottom, offset, unfold, view };
+ $.get(link, params, response => $target.parent().replaceWith(response));
+ }
+
+ openAnchoredDiff(anchoredDiff, cb) {
+ const diffTitle = $(`#file-path-${anchoredDiff}`);
+ const diffFile = diffTitle.closest('.diff-file');
+ const nothingHereBlock = $('.nothing-here-block:visible', diffFile);
+ if (nothingHereBlock.length) {
+ diffFile.singleFileDiff(true, cb);
+ } else {
+ cb();
+ }
+ }
+
+ handleClickLineNum(e) {
+ const hash = $(e.currentTarget).attr('href');
+ e.preventDefault();
+ if (window.history.pushState) {
+ window.history.pushState(null, null, hash);
+ } else {
+ window.location.hash = hash;
+ }
+ this.highlighSelectedLine();
+ }
+
+ diffViewType() {
+ return $('.inline-parallel-buttons a.active').data('view-type');
+ }
+
+ lineNumbers(line) {
+ if (!line.children().length) {
+ return [0, 0];
+ }
+ return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10));
+ }
+
+ highlighSelectedLine() {
+ const $diffFiles = $('.diff-file');
+ $diffFiles.find('.hll').removeClass('hll');
+
+ if (window.location.hash !== '') {
+ const hash = window.location.hash.replace('#', '');
+ $diffFiles
+ .find(`tr#${hash}:not(.match) td, td#${hash}, td[data-line-code="${hash}"]`)
+ .addClass('hll');
+ }
+ }
+ }
+
+ window.gl = window.gl || {};
+ window.gl.Diff = Diff;
+})();
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
index 29a12a2395b..c59d3996fab 100644
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
@@ -1,9 +1,16 @@
-/* eslint-disable */
-((w) => {
- w.CommentAndResolveBtn = Vue.extend({
+/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, semi, max-len */
+/* global Vue */
+/* global CommentsStore */
+
+(() => {
+ const CommentAndResolveBtn = Vue.extend({
props: {
discussionId: String,
- textareaIsEmpty: Boolean
+ },
+ data() {
+ return {
+ textareaIsEmpty: true
+ }
},
computed: {
discussion: function () {
@@ -35,7 +42,7 @@
}
}
},
- ready: function () {
+ mounted: function () {
const $textarea = $(`#new-discussion-note-form-${this.discussionId} .note-textarea`);
this.textareaIsEmpty = $textarea.val() === '';
@@ -47,4 +54,6 @@
$(`#new-discussion-note-form-${this.discussionId} .note-textarea`).off('input.comment-and-resolve-btn');
}
});
+
+ Vue.component('comment-and-resolve-btn', CommentAndResolveBtn);
})(window);
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
index 983e554b9c1..f47867fc3b0 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,6 +1,10 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, indent, space-before-function-paren, no-plusplus, no-lonely-if, no-continue, brace-style, max-len, quotes, semi */
+/* global Vue */
+/* global DiscussionMixins */
+/* global CommentsStore */
+
(() => {
- JumpToDiscussion = Vue.extend({
+ const JumpToDiscussion = Vue.extend({
mixins: [DiscussionMixins],
props: {
discussionId: String
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
index bcc052c7c8c..88a19fc6e1d 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
@@ -1,6 +1,11 @@
-/* eslint-disable */
-((w) => {
- w.ResolveBtn = Vue.extend({
+/* 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 ResolveBtn = Vue.extend({
props: {
noteId: Number,
discussionId: String,
@@ -54,7 +59,7 @@
},
methods: {
updateTooltip: function () {
- $(this.$els.button)
+ $(this.$refs.button)
.tooltip('hide')
.tooltip('fixTitle');
},
@@ -89,8 +94,8 @@
});
}
},
- compiled: function () {
- $(this.$els.button).tooltip({
+ mounted: function () {
+ $(this.$refs.button).tooltip({
container: 'body'
});
},
@@ -101,4 +106,6 @@
CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy);
}
});
-})(window);
+
+ Vue.component('resolve-btn', ResolveBtn);
+})();
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
index 24a99e23132..72cdae812bc 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */
+/* global Vue */
+/* global DiscussionMixins */
+/* global CommentsStore */
+
((w) => {
w.ResolveCount = Vue.extend({
mixins: [DiscussionMixins],
@@ -13,6 +17,9 @@
computed: {
allResolved: function () {
return this.resolvedDiscussionCount === this.discussionCount;
+ },
+ resolvedCountText() {
+ return this.discussionCount === 1 ? 'discussion' : 'discussions';
}
}
});
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
index 060034f049b..ee5f62b2d9e 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
@@ -1,6 +1,10 @@
-/* eslint-disable */
-((w) => {
- w.ResolveDiscussionBtn = Vue.extend({
+/* 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 ResolveDiscussionBtn = Vue.extend({
props: {
discussionId: String,
mergeRequestId: Number,
@@ -54,4 +58,6 @@
CommentsStore.createDiscussion(this.discussionId, this.canResolve);
}
});
-})(window);
+
+ Vue.component('resolve-discussion-btn', ResolveDiscussionBtn);
+})();
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
index 6149bfd052a..840b5aa922e 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, comma-dangle, new-cap, no-new */
+/* global Vue */
+/* global ResolveCount */
+
//= require vue
//= require vue-resource
//= require_directory ./models
@@ -8,24 +11,35 @@
//= require_directory ./components
$(() => {
- window.DiffNotesApp = new Vue({
- el: '#diff-notes-app',
- components: {
- 'resolve-btn': ResolveBtn,
- 'resolve-discussion-btn': ResolveDiscussionBtn,
- 'comment-and-resolve-btn': CommentAndResolveBtn
- },
- methods: {
- compileComponents: function () {
- const $components = $('resolve-btn, resolve-discussion-btn, jump-to-discussion');
- if ($components.length) {
- $components.each(function () {
- DiffNotesApp.$compile($(this).get(0));
- });
+ const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn';
+
+ window.gl = window.gl || {};
+ window.gl.diffNoteApps = {};
+
+ gl.diffNotesCompileComponents = () => {
+ const $components = $(COMPONENT_SELECTOR).filter(function () {
+ return $(this).closest('resolve-count').length !== 1;
+ });
+
+ if ($components) {
+ $components.each(function () {
+ const $this = $(this);
+ const noteId = $this.attr(':note-id');
+ const tmp = Vue.extend({
+ template: $this.get(0).outerHTML
+ });
+ const tmpApp = new tmp().$mount();
+
+ if (noteId) {
+ gl.diffNoteApps[`note_${noteId}`] = tmpApp;
}
- }
+
+ $this.replaceWith(tmpApp.$el);
+ });
}
- });
+ };
+
+ gl.diffNotesCompileComponents();
new Vue({
el: '#resolve-count-app',
diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
index 7a929017f36..a9ea18bf82b 100644
--- a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-plusplus, no-param-reassign, max-len */
+
((w) => {
w.DiscussionMixins = {
computed: {
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js.es6
index 439f55520ef..efcd46680a7 100644
--- a/app/assets/javascripts/diff_notes/models/discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/models/discussion.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, camelcase, guard-for-in, no-restricted-syntax, no-unused-vars, max-len */
+/* global Vue */
+/* global NoteModel */
+
class DiscussionModel {
constructor (discussionId) {
this.id = discussionId;
@@ -57,16 +60,19 @@ class DiscussionModel {
}
updateHeadline (data) {
- const $discussionHeadline = $(`.discussion[data-discussion-id="${this.id}"] .js-discussion-headline`);
+ const discussionSelector = `.discussion[data-discussion-id="${this.id}"]`;
+ const $discussionHeadline = $(`${discussionSelector} .js-discussion-headline`);
if (data.discussion_headline_html) {
if ($discussionHeadline.length) {
$discussionHeadline.replaceWith(data.discussion_headline_html);
} else {
- $(`.discussion[data-discussion-id="${this.id}"] .discussion-header`).append(data.discussion_headline_html);
+ $(`${discussionSelector} .discussion-header`).append(data.discussion_headline_html);
}
+
+ gl.utils.localTimeAgo($('.js-timeago', `${discussionSelector}`));
} else {
- $discussionHeadline.remove();
+ $discussionHeadline.remove();
}
}
@@ -74,7 +80,7 @@ class DiscussionModel {
if (!this.canResolve) {
return false;
}
-
+
for (const noteId in this.notes) {
const note = this.notes[noteId];
diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js.es6
index d0541b02632..e3bce1d2038 100644
--- a/app/assets/javascripts/diff_notes/models/note.js.es6
+++ b/app/assets/javascripts/diff_notes/models/note.js.es6
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable camelcase, no-unused-vars */
+
class NoteModel {
- constructor (discussionId, noteId, canResolve, resolved, resolved_by) {
+ constructor(discussionId, noteId, canResolve, resolved, resolved_by) {
this.discussionId = discussionId;
this.id = noteId;
this.canResolve = canResolve;
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6
index 86953ce7ffb..78c74985f78 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js.es6
+++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable class-methods-use-this, one-var, indent, camelcase, no-new, comma-dangle, semi, no-param-reassign, max-len */
+/* global Vue */
+/* global Flash */
+/* global CommentsStore */
+
((w) => {
class ResolveServiceClass {
constructor() {
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js.es6
index f42ca406bb1..1a7abbc6f75 100644
--- a/app/assets/javascripts/diff_notes/stores/comments.js.es6
+++ b/app/assets/javascripts/diff_notes/stores/comments.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable object-shorthand, func-names, camelcase, prefer-const, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */
+/* global Vue */
+/* global DiscussionModel */
+
((w) => {
w.CommentsStore = {
state: {},
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 8e4fd1f19ba..1e259a16f06 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -1,4 +1,42 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len, padded-blocks */
+/* global UsernameValidator */
+/* global ActiveTabMemoizer */
+/* global ShortcutsNavigation */
+/* global Build */
+/* global Issuable */
+/* global Issue */
+/* global ShortcutsIssuable */
+/* global ZenMode */
+/* global Milestone */
+/* global GLForm */
+/* global IssuableForm */
+/* global LabelsSelect */
+/* global MilestoneSelect */
+/* global MergedButtons */
+/* global Commit */
+/* global NotificationsForm */
+/* global TreeView */
+/* global NotificationsDropdown */
+/* global UsersSelect */
+/* global GroupAvatar */
+/* global LineHighlighter */
+/* global ShortcutsBlob */
+/* global ProjectFork */
+/* global BuildArtifacts */
+/* global GroupsSelect */
+/* global Search */
+/* global Admin */
+/* global NamespaceSelects */
+/* global ShortcutsDashboardNavigation */
+/* global Project */
+/* global ProjectAvatar */
+/* global CompareAutocomplete */
+/* global ProjectNew */
+/* global Star */
+/* global ProjectShow */
+/* global Labels */
+/* global Shortcuts */
+
(function() {
var Dispatcher;
@@ -24,15 +62,21 @@
switch (page) {
case 'sessions:new':
new UsernameValidator();
+ new ActiveTabMemoizer();
break;
case 'projects:boards:show':
case 'projects:boards:index':
shortcut_handler = new ShortcutsNavigation();
break;
+ case 'projects:builds:show':
+ new Build();
+ break;
case 'projects:merge_requests:index':
case 'projects:issues:index':
Issuable.init();
- new gl.IssuableBulkActions();
+ new gl.IssuableBulkActions({
+ prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
+ });
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:issues:show':
@@ -58,7 +102,7 @@
new ZenMode();
break;
case 'projects:compare:show':
- new Diff();
+ new gl.Diff();
break;
case 'projects:issues:new':
case 'projects:issues:edit':
@@ -71,7 +115,7 @@
break;
case 'projects:merge_requests:new':
case 'projects:merge_requests:edit':
- new Diff();
+ new gl.Diff();
shortcut_handler = new ShortcutsNavigation();
new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form'));
@@ -88,7 +132,7 @@
new GLForm($('.release-form'));
break;
case 'projects:merge_requests:show':
- new Diff();
+ new gl.Diff();
shortcut_handler = new ShortcutsIssuable(true);
new ZenMode();
new MergedButtons();
@@ -98,23 +142,19 @@
new MergedButtons();
break;
case "projects:merge_requests:diffs":
- new Diff();
+ new gl.Diff();
new ZenMode();
new MergedButtons();
break;
- case 'projects:merge_requests:index':
- shortcut_handler = new ShortcutsNavigation();
- Issuable.init();
- break;
case 'dashboard:activity':
- new Activities();
+ new gl.Activities();
break;
case 'dashboard:projects:starred':
- new Activities();
+ new gl.Activities();
break;
case 'projects:commit:show':
new Commit();
- new Diff();
+ new gl.Diff();
new ZenMode();
shortcut_handler = new ShortcutsNavigation();
break;
@@ -132,11 +172,21 @@
new TreeView();
}
break;
+ case 'projects:pipelines:builds':
case 'projects:pipelines:show':
- new gl.Pipelines();
+ const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
+
+ new gl.Pipelines({
+ initTabs: true,
+ tabsOptions: {
+ action: controllerAction,
+ defaultAction: 'pipelines',
+ parentEl: '.pipelines-tabs',
+ },
+ });
break;
case 'groups:activity':
- new Activities();
+ new gl.Activities();
break;
case 'groups:show':
shortcut_handler = new ShortcutsNavigation();
@@ -205,8 +255,8 @@
new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList();
break;
- case 'projects:cycle_analytics:show':
- new gl.CycleAnalytics();
+ case 'projects:variables:index':
+ new gl.ProjectVariables();
break;
}
switch (path.first()) {
@@ -259,7 +309,7 @@
new NotificationsDropdown();
break;
case 'wikis':
- new Wikis();
+ new gl.Wikis();
shortcut_handler = new ShortcutsNavigation();
new ZenMode();
new GLForm($('.wiki-form'));
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 1a0aa9757ba..56cb39be642 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, no-plusplus, prefer-arrow-callback, padded-blocks */
+/* global Dropzone */
/*= require preview_markdown */
diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6
index fd7f961aab9..2b7d57d86c6 100644
--- a/app/assets/javascripts/due_date_select.js.es6
+++ b/app/assets/javascripts/due_date_select.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, prefer-const, padded-blocks, no-unused-vars, no-underscore-dangle, no-new, max-len, semi, no-sequences, no-unused-expressions, no-param-reassign */
+
(function(global) {
class DueDateSelect {
constructor({ $dropdown, $loading } = {}) {
@@ -145,25 +146,19 @@
class DueDateSelectors {
constructor() {
- this.initMilestoneDueDate();
+ this.initMilestoneDatePicker();
this.initIssuableSelect();
}
- initMilestoneDueDate() {
- const $datePicker = $('.datepicker');
+ initMilestoneDatePicker() {
+ $('.datepicker').datepicker({
+ dateFormat: 'yy-mm-dd'
+ });
- if ($datePicker.length) {
- const $dueDate = $('#milestone_due_date');
- $datePicker.datepicker({
- dateFormat: 'yy-mm-dd',
- onSelect: (dateText, inst) => {
- $dueDate.val(dateText);
- }
- }).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val()));
- }
- $('.js-clear-due-date').on('click', (e) => {
+ $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => {
e.preventDefault();
- $.datepicker._clearDate($datePicker);
+ const datepicker = $(e.target).siblings('.datepicker');
+ $.datepicker._clearDate(datepicker);
});
}
diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6
new file mode 100644
index 00000000000..88c3d257cea
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment.js.es6
@@ -0,0 +1,254 @@
+/* eslint-disable no-param-reassign */
+/* global Vue */
+/* global EnvironmentsService */
+
+//= require vue
+//= require vue-resource
+//= require_tree ../services/
+//= require ./environment_item
+
+(() => {
+ window.gl = window.gl || {};
+
+ /**
+ * Given the visibility prop provided by the url query parameter and which
+ * changes according to the active tab we need to filter which environments
+ * should be visible.
+ *
+ * The environments array is a recursive tree structure and we need to filter
+ * both root level environments and children environments.
+ *
+ * In order to acomplish that, both `filterState` and `filterEnvironmnetsByState`
+ * functions work together.
+ * The first one works as the filter that verifies if the given environment matches
+ * the given state.
+ * The second guarantees both root level and children elements are filtered as well.
+ */
+
+ const filterState = state => environment => environment.state === state && environment;
+ /**
+ * Given the filter function and the array of environments will return only
+ * the environments that match the state provided to the filter function.
+ *
+ * @param {Function} fn
+ * @param {Array} array
+ * @return {Array}
+ */
+ const filterEnvironmnetsByState = (fn, arr) => arr.map((item) => {
+ if (item.children) {
+ const filteredChildren = filterEnvironmnetsByState(fn, item.children).filter(Boolean);
+ if (filteredChildren.length) {
+ item.children = filteredChildren;
+ return item;
+ }
+ }
+ return fn(item);
+ }).filter(Boolean);
+
+ window.gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', {
+ props: {
+ store: {
+ type: Object,
+ required: true,
+ default: () => ({}),
+ },
+ },
+
+ components: {
+ 'environment-item': window.gl.environmentsList.EnvironmentItem,
+ },
+
+ data() {
+ const environmentsData = document.querySelector('#environments-list-view').dataset;
+
+ return {
+ state: this.store.state,
+ visibility: 'available',
+ isLoading: false,
+ cssContainerClass: environmentsData.cssClass,
+ endpoint: environmentsData.environmentsDataEndpoint,
+ canCreateDeployment: environmentsData.canCreateDeployment,
+ canReadEnvironment: environmentsData.canReadEnvironment,
+ canCreateEnvironment: environmentsData.canCreateEnvironment,
+ projectEnvironmentsPath: environmentsData.projectEnvironmentsPath,
+ projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
+ newEnvironmentPath: environmentsData.newEnvironmentPath,
+ helpPagePath: environmentsData.helpPagePath,
+ commitIconSvg: environmentsData.commitIconSvg,
+ playIconSvg: environmentsData.playIconSvg,
+ };
+ },
+
+ computed: {
+ filteredEnvironments() {
+ return filterEnvironmnetsByState(filterState(this.visibility), this.state.environments);
+ },
+
+ scope() {
+ return this.$options.getQueryParameter('scope');
+ },
+
+ canReadEnvironmentParsed() {
+ return this.$options.convertPermissionToBoolean(this.canReadEnvironment);
+ },
+
+ canCreateDeploymentParsed() {
+ return this.$options.convertPermissionToBoolean(this.canCreateDeployment);
+ },
+
+ canCreateEnvironmentParsed() {
+ return this.$options.convertPermissionToBoolean(this.canCreateEnvironment);
+ },
+ },
+
+ /**
+ * Fetches all the environmnets and stores them.
+ * Toggles loading property.
+ */
+ created() {
+ gl.environmentsService = new EnvironmentsService(this.endpoint);
+
+ const scope = this.$options.getQueryParameter('scope');
+ if (scope) {
+ this.visibility = scope;
+ }
+
+ this.isLoading = true;
+
+ return gl.environmentsService.all()
+ .then(resp => resp.json())
+ .then((json) => {
+ this.store.storeEnvironments(json);
+ this.isLoading = false;
+ });
+ },
+
+ /**
+ * Transforms the url parameter into an object and
+ * returns the one requested.
+ *
+ * @param {String} param
+ * @returns {String} The value of the requested parameter.
+ */
+ getQueryParameter(parameter) {
+ return window.location.search.substring(1).split('&').reduce((acc, param) => {
+ const paramSplited = param.split('=');
+ acc[paramSplited[0]] = paramSplited[1];
+ return acc;
+ }, {})[parameter];
+ },
+
+ /**
+ * Converts permission provided as strings to booleans.
+ * @param {String} string
+ * @returns {Boolean}
+ */
+ convertPermissionToBoolean(string) {
+ return string === 'true';
+ },
+
+ methods: {
+ toggleRow(model) {
+ return this.store.toggleFolder(model.name);
+ },
+ },
+
+ template: `
+ <div :class="cssContainerClass">
+ <div class="top-area">
+ <ul v-if="!isLoading" class="nav-links">
+ <li v-bind:class="{ 'active': scope === undefined }">
+ <a :href="projectEnvironmentsPath">
+ Available
+ <span class="badge js-available-environments-count">
+ {{state.availableCounter}}
+ </span>
+ </a>
+ </li><li v-bind:class="{ 'active' : scope === 'stopped' }">
+ <a :href="projectStoppedEnvironmentsPath">
+ Stopped
+ <span class="badge js-stopped-environments-count">
+ {{state.stoppedCounter}}
+ </span>
+ </a>
+ </li>
+ </ul>
+ <div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls">
+ <a :href="newEnvironmentPath" class="btn btn-create">
+ New environment
+ </a>
+ </div>
+ </div>
+
+ <div class="environments-container">
+ <div class="environments-list-loading text-center" v-if="isLoading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+
+ <div class="blank-state blank-state-no-icon"
+ v-if="!isLoading && state.environments.length === 0">
+ <h2 class="blank-state-title">
+ You don't have any environments right now.
+ </h2>
+ <p class="blank-state-text">
+ Environments are places where code gets deployed, such as staging or production.
+ <br />
+ <a :href="helpPagePath">
+ Read more about environments
+ </a>
+ </p>
+
+ <a
+ v-if="canCreateEnvironmentParsed"
+ :href="newEnvironmentPath"
+ class="btn btn-create">
+ New Environment
+ </a>
+ </div>
+
+ <div class="table-holder"
+ v-if="!isLoading && state.environments.length > 0">
+ <table class="table ci-table environments">
+ <thead>
+ <tr>
+ <th class="environments-name">Environment</th>
+ <th class="environments-deploy">Last deployment</th>
+ <th class="environments-build">Build</th>
+ <th class="environments-commit">Commit</th>
+ <th class="environments-date"></th>
+ <th class="hidden-xs environments-actions"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <template v-for="model in filteredEnvironments"
+ v-bind:model="model">
+
+ <tr
+ is="environment-item"
+ :model="model"
+ :toggleRow="toggleRow.bind(model)"
+ :can-create-deployment="canCreateDeploymentParsed"
+ :can-read-environment="canReadEnvironmentParsed"
+ :play-icon-svg="playIconSvg"
+ :commit-icon-svg="commitIconSvg"></tr>
+
+ <tr v-if="model.isOpen && model.children && model.children.length > 0"
+ is="environment-item"
+ v-for="children in model.children"
+ :model="children"
+ :toggleRow="toggleRow.bind(children)"
+ :can-create-deployment="canCreateDeploymentParsed"
+ :can-read-environment="canReadEnvironmentParsed"
+ :play-icon-svg="playIconSvg"
+ :commit-icon-svg="commitIconSvg">
+ </tr>
+
+ </template>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6
new file mode 100644
index 00000000000..7c743705d51
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_actions.js.es6
@@ -0,0 +1,49 @@
+/*= require vue */
+/* global Vue */
+
+(() => {
+ window.gl = window.gl || {};
+ window.gl.environmentsList = window.gl.environmentsList || {};
+
+ window.gl.environmentsList.ActionsComponent = Vue.component('actions-component', {
+ props: {
+ actions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+
+ playIconSvg: {
+ type: String,
+ required: false,
+ },
+ },
+
+ template: `
+ <div class="inline">
+ <div class="dropdown">
+ <a class="dropdown-new btn btn-default" data-toggle="dropdown">
+ <span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span>
+ <i class="fa fa-caret-down"></i>
+ </a>
+
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for="action in actions">
+ <a :href="action.play_path"
+ data-method="post"
+ rel="nofollow"
+ class="js-manual-action-link">
+
+ <span class="js-action-play-icon-container" v-html="playIconSvg"></span>
+
+ <span>
+ {{action.name}}
+ </span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js.es6
new file mode 100644
index 00000000000..aed65b33c04
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_external_url.js.es6
@@ -0,0 +1,22 @@
+/*= require vue */
+/* global Vue */
+
+(() => {
+ window.gl = window.gl || {};
+ window.gl.environmentsList = window.gl.environmentsList || {};
+
+ window.gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', {
+ props: {
+ externalUrl: {
+ type: String,
+ default: '',
+ },
+ },
+
+ template: `
+ <a class="btn external_url" :href="externalUrl" target="_blank">
+ <i class="fa fa-external-link"></i>
+ </a>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6
new file mode 100644
index 00000000000..4674d5202e6
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_item.js.es6
@@ -0,0 +1,521 @@
+/* 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 */
+
+(() => {
+ /**
+ * Envrionment Item Component
+ *
+ * Used in a hierarchical structure to show folders with children
+ * in a table.
+ * Recursive component based on [Tree View](https://vuejs.org/examples/tree-view.html)
+ *
+ * See this [issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/22539)
+ * for more information.15
+ */
+
+ window.gl = window.gl || {};
+ window.gl.environmentsList = window.gl.environmentsList || {};
+ window.gl.environmentsList.timeagoInstance = new timeago(); // eslint-disable-line
+
+ gl.environmentsList.EnvironmentItem = Vue.component('environment-item', {
+
+ components: {
+ 'commit-component': window.gl.CommitComponent,
+ 'actions-component': window.gl.environmentsList.ActionsComponent,
+ 'external-url-component': window.gl.environmentsList.ExternalUrlComponent,
+ 'stop-component': window.gl.environmentsList.StopComponent,
+ 'rollback-component': window.gl.environmentsList.RollbackComponent,
+ },
+
+ props: {
+ model: {
+ type: Object,
+ required: true,
+ default: () => ({}),
+ },
+
+ toggleRow: {
+ type: Function,
+ required: false,
+ },
+
+ canCreateDeployment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ canReadEnvironment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ commitIconSvg: {
+ type: String,
+ required: false,
+ },
+
+ playIconSvg: {
+ type: String,
+ required: false,
+ },
+ },
+
+ data() {
+ return {
+ rowClass: {
+ 'children-row': this.model['vue-isChildren'],
+ },
+ };
+ },
+
+ computed: {
+
+ /**
+ * If an item has a `children` entry it means it is a folder.
+ * Folder items have different behaviours - it is possible to toggle
+ * them and show their children.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ isFolder() {
+ return this.model.children && this.model.children.length > 0;
+ },
+
+ /**
+ * If an item is inside a folder structure will return true.
+ * Used for css purposes.
+ *
+ * @returns {Boolean|undefined}
+ */
+ isChildren() {
+ return this.model['vue-isChildren'];
+ },
+
+ /**
+ * Counts the number of environments in each folder.
+ * Used to show a badge with the counter.
+ *
+ * @returns {Number|Undefined} The number of environments for the current folder.
+ */
+ childrenCounter() {
+ return this.model.children && this.model.children.length;
+ },
+
+ /**
+ * Verifies if `last_deployment` key exists in the current Envrionment.
+ * This key is required to render most of the html - this method works has
+ * an helper.
+ *
+ * @returns {Boolean}
+ */
+ hasLastDeploymentKey() {
+ if (this.model.last_deployment &&
+ !this.$options.isObjectEmpty(this.model.last_deployment)) {
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Verifies is the given environment has manual actions.
+ * Used to verify if we should render them or nor.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ hasManualActions() {
+ return this.model.last_deployment && this.model.last_deployment.manual_actions &&
+ this.model.last_deployment.manual_actions.length > 0;
+ },
+
+ /**
+ * Returns the value of the `stoppable?` key provided in the response.
+ *
+ * @returns {Boolean}
+ */
+ isStoppable() {
+ return this.model['stoppable?'];
+ },
+
+ /**
+ * Verifies if the `deployable` key is present in `last_deployment` key.
+ * Used to verify whether we should or not render the rollback partial.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ canRetry() {
+ return this.hasLastDeploymentKey &&
+ this.model.last_deployment &&
+ this.model.last_deployment.deployable;
+ },
+
+ /**
+ * Verifies if the date to be shown is present.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ canShowDate() {
+ return this.model.last_deployment &&
+ this.model.last_deployment.deployable &&
+ this.model.last_deployment.deployable !== undefined;
+ },
+
+ /**
+ * Human readable date.
+ *
+ * @returns {String}
+ */
+ createdDate() {
+ return window.gl.environmentsList.timeagoInstance.format(
+ this.model.last_deployment.deployable.created_at,
+ );
+ },
+
+ /**
+ * Returns the manual actions with the name parsed.
+ *
+ * @returns {Array.<Object>|Undefined}
+ */
+ manualActions() {
+ if (this.hasManualActions) {
+ return this.model.last_deployment.manual_actions.map((action) => {
+ const parsedAction = {
+ name: gl.text.humanize(action.name),
+ play_path: action.play_path,
+ };
+ return parsedAction;
+ });
+ }
+ return [];
+ },
+
+ /**
+ * Builds the string used in the user image alt attribute.
+ *
+ * @returns {String}
+ */
+ userImageAltDescription() {
+ if (this.model.last_deployment &&
+ this.model.last_deployment.user &&
+ this.model.last_deployment.user.username) {
+ return `${this.model.last_deployment.user.username}'s avatar'`;
+ }
+ return '';
+ },
+
+ /**
+ * If provided, returns the commit tag.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTag() {
+ if (this.model.last_deployment &&
+ this.model.last_deployment.tag) {
+ return this.model.last_deployment.tag;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit ref.
+ *
+ * @returns {Object|Undefined}
+ */
+ commitRef() {
+ if (this.model.last_deployment && this.model.last_deployment.ref) {
+ return this.model.last_deployment.ref;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit url.
+ *
+ * @returns {String|Undefined}
+ */
+ commitUrl() {
+ if (this.model.last_deployment &&
+ this.model.last_deployment.commit &&
+ this.model.last_deployment.commit.commit_path) {
+ return this.model.last_deployment.commit.commit_path;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit short sha.
+ *
+ * @returns {String|Undefined}
+ */
+ commitShortSha() {
+ if (this.model.last_deployment &&
+ this.model.last_deployment.commit &&
+ this.model.last_deployment.commit.short_id) {
+ return this.model.last_deployment.commit.short_id;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit title.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTitle() {
+ if (this.model.last_deployment &&
+ this.model.last_deployment.commit &&
+ this.model.last_deployment.commit.title) {
+ return this.model.last_deployment.commit.title;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit tag.
+ *
+ * @returns {Object|Undefined}
+ */
+ commitAuthor() {
+ if (this.model.last_deployment &&
+ this.model.last_deployment.commit &&
+ this.model.last_deployment.commit.author) {
+ return this.model.last_deployment.commit.author;
+ }
+
+ return undefined;
+ },
+
+ /**
+ * Verifies if the `retry_path` key is present and returns its value.
+ *
+ * @returns {String|Undefined}
+ */
+ retryUrl() {
+ if (this.model.last_deployment &&
+ this.model.last_deployment.deployable &&
+ this.model.last_deployment.deployable.retry_path) {
+ return this.model.last_deployment.deployable.retry_path;
+ }
+ return undefined;
+ },
+
+ /**
+ * Verifies if the `last?` key is present and returns its value.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ isLastDeployment() {
+ return this.model.last_deployment && this.model.last_deployment['last?'];
+ },
+
+ /**
+ * Builds the name of the builds needed to display both the name and the id.
+ *
+ * @returns {String}
+ */
+ buildName() {
+ if (this.model.last_deployment &&
+ this.model.last_deployment.deployable) {
+ return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`;
+ }
+ return '';
+ },
+
+ /**
+ * Builds the needed string to show the internal id.
+ *
+ * @returns {String}
+ */
+ deploymentInternalId() {
+ if (this.model.last_deployment &&
+ this.model.last_deployment.iid) {
+ return `#${this.model.last_deployment.iid}`;
+ }
+ return '';
+ },
+
+ /**
+ * Verifies if the user object is present under last_deployment object.
+ *
+ * @returns {Boolean}
+ */
+ deploymentHasUser() {
+ return !this.$options.isObjectEmpty(this.model.last_deployment) &&
+ !this.$options.isObjectEmpty(this.model.last_deployment.user);
+ },
+
+ /**
+ * Returns the user object nested with the last_deployment object.
+ * Used to render the template.
+ *
+ * @returns {Object}
+ */
+ deploymentUser() {
+ if (!this.$options.isObjectEmpty(this.model.last_deployment) &&
+ !this.$options.isObjectEmpty(this.model.last_deployment.user)) {
+ return this.model.last_deployment.user;
+ }
+ return {};
+ },
+
+ /**
+ * Verifies if the build name column should be rendered by verifing
+ * if all the information needed is present
+ * and if the environment is not a folder.
+ *
+ * @returns {Boolean}
+ */
+ shouldRenderBuildName() {
+ return !this.isFolder &&
+ !this.$options.isObjectEmpty(this.model.last_deployment) &&
+ !this.$options.isObjectEmpty(this.model.last_deployment.deployable);
+ },
+
+ /**
+ * Verifies if deplyment internal ID should be rendered by verifing
+ * if all the information needed is present
+ * and if the environment is not a folder.
+ *
+ * @returns {Boolean}
+ */
+ shouldRenderDeploymentID() {
+ return !this.isFolder &&
+ !this.$options.isObjectEmpty(this.model.last_deployment) &&
+ this.model.last_deployment.iid !== undefined;
+ },
+ },
+
+ /**
+ * Helper to verify if certain given object are empty.
+ * Should be replaced by lodash _.isEmpty - https://lodash.com/docs/4.17.2#isEmpty
+ * @param {Object} object
+ * @returns {Bollean}
+ */
+ isObjectEmpty(object) {
+ for (const key in object) { // eslint-disable-line
+ if (hasOwnProperty.call(object, key)) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ template: `
+ <tr>
+ <td v-bind:class="{ 'children-row': isChildren}">
+ <a v-if="!isFolder"
+ class="environment-name"
+ :href="model.environment_path">
+ {{model.name}}
+ </a>
+ <span v-else v-on:click="toggleRow(model)" class="folder-name">
+ <span class="folder-icon">
+ <i v-show="model.isOpen" class="fa fa-caret-down"></i>
+ <i v-show="!model.isOpen" class="fa fa-caret-right"></i>
+ </span>
+
+ <span>
+ {{model.name}}
+ </span>
+
+ <span class="badge">
+ {{childrenCounter}}
+ </span>
+ </span>
+ </td>
+
+ <td class="deployment-column">
+ <span v-if="shouldRenderDeploymentID">
+ {{deploymentInternalId}}
+ </span>
+
+ <span v-if="!isFolder && deploymentHasUser">
+ by
+ <a :href="deploymentUser.web_url" class="js-deploy-user-container">
+ <img class="avatar has-tooltip s20"
+ :src="deploymentUser.avatar_url"
+ :alt="userImageAltDescription"
+ :title="deploymentUser.username" />
+ </a>
+ </span>
+ </td>
+
+ <td class="environments-build-cell">
+ <a v-if="shouldRenderBuildName"
+ class="build-link"
+ :href="model.last_deployment.deployable.build_path">
+ {{buildName}}
+ </a>
+ </td>
+
+ <td>
+ <div v-if="!isFolder && hasLastDeploymentKey" class="js-commit-component">
+ <commit-component
+ :tag="commitTag"
+ :commit-ref="commitRef"
+ :commit-url="commitUrl"
+ :short-sha="commitShortSha"
+ :title="commitTitle"
+ :author="commitAuthor"
+ :commit-icon-svg="commitIconSvg">
+ </commit-component>
+ </div>
+ <p v-if="!isFolder && !hasLastDeploymentKey" class="commit-title">
+ No deployments yet
+ </p>
+ </td>
+
+ <td>
+ <span
+ v-if="!isFolder && canShowDate"
+ class="environment-created-date-timeago">
+ {{createdDate}}
+ </span>
+ </td>
+
+ <td class="hidden-xs">
+ <div v-if="!isFolder">
+ <div v-if="hasManualActions && canCreateDeployment"
+ class="inline js-manual-actions-container">
+ <actions-component
+ :play-icon-svg="playIconSvg"
+ :actions="manualActions">
+ </actions-component>
+ </div>
+
+ <div v-if="model.external_url && canReadEnvironment"
+ class="inline js-external-url-container">
+ <external-url-component
+ :external-url="model.external_url">
+ </external-url-component>
+ </div>
+
+ <div v-if="isStoppable && canCreateDeployment"
+ class="inline js-stop-component-container">
+ <stop-component
+ :stop-url="model.stop_path">
+ </stop-component>
+ </div>
+
+ <div v-if="canRetry && canCreateDeployment"
+ class="inline js-rollback-component-container">
+ <rollback-component
+ :is-last-deployment="isLastDeployment"
+ :retry-url="retryUrl">
+ </rollback-component>
+ </div>
+ </div>
+ </td>
+ </tr>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js.es6
new file mode 100644
index 00000000000..6d4e8fad604
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_rollback.js.es6
@@ -0,0 +1,32 @@
+/*= require vue */
+/* global Vue */
+
+(() => {
+ window.gl = window.gl || {};
+ window.gl.environmentsList = window.gl.environmentsList || {};
+
+ window.gl.environmentsList.RollbackComponent = Vue.component('rollback-component', {
+ props: {
+ retryUrl: {
+ type: String,
+ default: '',
+ },
+
+ isLastDeployment: {
+ type: Boolean,
+ default: true,
+ },
+ },
+
+ template: `
+ <a class="btn" :href="retryUrl" data-method="post" rel="nofollow">
+ <span v-if="isLastDeployment">
+ Re-deploy
+ </span>
+ <span v-else>
+ Rollback
+ </span>
+ </a>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6
new file mode 100644
index 00000000000..7292f924e5c
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_stop.js.es6
@@ -0,0 +1,26 @@
+/*= require vue */
+/* global Vue */
+
+(() => {
+ window.gl = window.gl || {};
+ window.gl.environmentsList = window.gl.environmentsList || {};
+
+ window.gl.environmentsList.StopComponent = Vue.component('stop-component', {
+ props: {
+ stopUrl: {
+ type: String,
+ default: '',
+ },
+ },
+
+ template: `
+ <a class="btn stop-env-link"
+ :href="stopUrl"
+ data-confirm="Are you sure you want to stop this environment?"
+ data-method="post"
+ rel="nofollow">
+ <i class="fa fa-stop stop-env-icon"></i>
+ </a>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6
new file mode 100644
index 00000000000..20eee7976ec
--- /dev/null
+++ b/app/assets/javascripts/environments/environments_bundle.js.es6
@@ -0,0 +1,21 @@
+//= require vue
+//= require_tree ./stores/
+//= require ./components/environment
+//= require ./vue_resource_interceptor
+
+
+$(() => {
+ window.gl = window.gl || {};
+
+ if (window.gl.EnvironmentsListApp) {
+ window.gl.EnvironmentsListApp.$destroy(true);
+ }
+ const Store = window.gl.environmentsList.EnvironmentsStore;
+
+ window.gl.EnvironmentsListApp = new window.gl.environmentsList.EnvironmentsComponent({
+ el: document.querySelector('#environments-list-view'),
+ propsData: {
+ store: Store.create(),
+ },
+ });
+});
diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6
new file mode 100644
index 00000000000..15ec7b76c3d
--- /dev/null
+++ b/app/assets/javascripts/environments/services/environments_service.js.es6
@@ -0,0 +1,22 @@
+/* globals Vue */
+/* eslint-disable no-unused-vars, no-param-reassign */
+class EnvironmentsService {
+
+ constructor(root) {
+ Vue.http.options.root = root;
+
+ this.environments = Vue.resource(root);
+
+ Vue.http.interceptors.push((request, next) => {
+ // needed in order to not break the tests.
+ if ($.rails) {
+ request.headers['X-CSRF-Token'] = $.rails.csrfToken();
+ }
+ next();
+ });
+ }
+
+ all() {
+ return this.environments.get();
+ }
+}
diff --git a/app/assets/javascripts/environments/stores/environments_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js.es6
new file mode 100644
index 00000000000..0204a903ab5
--- /dev/null
+++ b/app/assets/javascripts/environments/stores/environments_store.js.es6
@@ -0,0 +1,131 @@
+/* eslint-disable no-param-reassign */
+(() => {
+ window.gl = window.gl || {};
+ window.gl.environmentsList = window.gl.environmentsList || {};
+
+ gl.environmentsList.EnvironmentsStore = {
+ state: {},
+
+ create() {
+ this.state.environments = [];
+ this.state.stoppedCounter = 0;
+ this.state.availableCounter = 0;
+
+ return this;
+ },
+
+ /**
+ * In order to display a tree view we need to modify the received
+ * data in to a tree structure based on `environment_type`
+ * sorted alphabetically.
+ * In each children a `vue-` property will be added. This property will be
+ * used to know if an item is a children mostly for css purposes. This is
+ * needed because the children row is a fragment instance and therfore does
+ * not accept non-prop attributes.
+ *
+ *
+ * @example
+ * it will transform this:
+ * [
+ * { name: "environment", environment_type: "review" },
+ * { name: "environment_1", environment_type: null }
+ * { name: "environment_2, environment_type: "review" }
+ * ]
+ * into this:
+ * [
+ * { name: "review", children:
+ * [
+ * { name: "environment", environment_type: "review", vue-isChildren: true},
+ * { name: "environment_2", environment_type: "review", vue-isChildren: true}
+ * ]
+ * },
+ * {name: "environment_1", environment_type: null}
+ * ]
+ *
+ *
+ * @param {Array} environments List of environments.
+ * @returns {Array} Tree structured array with the received environments.
+ */
+ storeEnvironments(environments = []) {
+ this.state.stoppedCounter = this.countByState(environments, 'stopped');
+ this.state.availableCounter = this.countByState(environments, 'available');
+
+ const environmentsTree = environments.reduce((acc, environment) => {
+ if (environment.environment_type !== null) {
+ const occurs = acc.filter(element => element.children &&
+ element.name === environment.environment_type);
+
+ environment['vue-isChildren'] = true;
+
+ if (occurs.length) {
+ acc[acc.indexOf(occurs[0])].children.push(environment);
+ acc[acc.indexOf(occurs[0])].children.sort(this.sortByName);
+ } else {
+ acc.push({
+ name: environment.environment_type,
+ children: [environment],
+ isOpen: false,
+ 'vue-isChildren': environment['vue-isChildren'],
+ });
+ }
+ } else {
+ acc.push(environment);
+ }
+
+ return acc;
+ }, []).sort(this.sortByName);
+
+ this.state.environments = environmentsTree;
+
+ return environmentsTree;
+ },
+
+ /**
+ * Toggles folder open property given the environment type.
+ *
+ * @param {String} envType
+ * @return {Array}
+ */
+ toggleFolder(envType) {
+ const environments = this.state.environments;
+
+ const environmentsCopy = environments.map((env) => {
+ if (env['vue-isChildren'] && env.name === envType) {
+ env.isOpen = !env.isOpen;
+ }
+
+ return env;
+ });
+
+ this.state.environments = environmentsCopy;
+
+ return environmentsCopy;
+ },
+
+ /**
+ * Given an array of environments, returns the number of environments
+ * that have the given state.
+ *
+ * @param {Array} environments
+ * @param {String} state
+ * @returns {Number}
+ */
+ countByState(environments, state) {
+ return environments.filter(env => env.state === state).length;
+ },
+
+ /**
+ * Sorts the two objects provided by their name.
+ *
+ * @param {Object} a
+ * @param {Object} b
+ * @returns {Number}
+ */
+ sortByName(a, b) {
+ const nameA = a.name.toUpperCase();
+ const nameB = b.name.toUpperCase();
+
+ return nameA < nameB ? -1 : nameA > nameB ? 1 : 0; // eslint-disable-line
+ },
+ };
+})();
diff --git a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 b/app/assets/javascripts/environments/vue_resource_interceptor.js.es6
new file mode 100644
index 00000000000..406bdbc1c7d
--- /dev/null
+++ b/app/assets/javascripts/environments/vue_resource_interceptor.js.es6
@@ -0,0 +1,12 @@
+/* global Vue */
+Vue.http.interceptors.push((request, next) => {
+ Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
+
+ next((response) => {
+ if (typeof response.data === 'string') {
+ response.data = JSON.parse(response.data); // eslint-disable-line
+ }
+
+ Vue.activeResources--; // eslint-disable-line
+ });
+});
diff --git a/app/assets/javascripts/extensions/array.js b/app/assets/javascripts/extensions/array.js
deleted file mode 100644
index 4c9e219aa43..00000000000
--- a/app/assets/javascripts/extensions/array.js
+++ /dev/null
@@ -1,8 +0,0 @@
-/* eslint-disable */
-Array.prototype.first = function() {
- return this[0];
-}
-
-Array.prototype.last = function() {
- return this[this.length-1];
-}
diff --git a/app/assets/javascripts/extensions/array.js.es6 b/app/assets/javascripts/extensions/array.js.es6
new file mode 100644
index 00000000000..717566a4715
--- /dev/null
+++ b/app/assets/javascripts/extensions/array.js.es6
@@ -0,0 +1,24 @@
+/* eslint-disable no-extend-native, func-names, space-before-function-paren, semi, space-infix-ops, max-len */
+Array.prototype.first = function() {
+ return this[0];
+}
+
+Array.prototype.last = function() {
+ return this[this.length-1];
+}
+
+Array.prototype.find = Array.prototype.find || function(predicate, ...args) {
+ if (!this) throw new TypeError('Array.prototype.find called on null or undefined');
+ if (typeof predicate !== 'function') throw new TypeError('predicate must be a function');
+
+ const list = Object(this);
+ const thisArg = args[1];
+ let value = {};
+
+ for (let i = 0; i < list.length; i += 1) {
+ value = list[i];
+ if (predicate.call(thisArg, value, i, list)) return value;
+ }
+
+ return undefined;
+};
diff --git a/app/assets/javascripts/extensions/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6
index afb2f0d6956..3f12ad9ff9f 100644
--- a/app/assets/javascripts/extensions/element.js.es6
+++ b/app/assets/javascripts/extensions/element.js.es6
@@ -1,9 +1,20 @@
/* global Element */
-/* eslint-disable consistent-return, max-len */
+/* eslint-disable consistent-return, max-len, no-empty, no-plusplus, func-names */
-Element.prototype.matches = Element.prototype.matches || Element.prototype.msMatchesSelector;
-
-Element.prototype.closest = function closest(selector, selectedElement = this) {
+Element.prototype.closest = Element.prototype.closest || function closest(selector, selectedElement = this) {
if (!selectedElement) return;
return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement);
};
+
+Element.prototype.matches = Element.prototype.matches ||
+ Element.prototype.matchesSelector ||
+ Element.prototype.mozMatchesSelector ||
+ Element.prototype.msMatchesSelector ||
+ Element.prototype.oMatchesSelector ||
+ Element.prototype.webkitMatchesSelector ||
+ function (s) {
+ const matches = (this.document || this.ownerDocument).querySelectorAll(s);
+ let i = matches.length;
+ while (--i >= 0 && matches.item(i) !== this) {}
+ return i > -1;
+ };
diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js
index 623a80b7053..cdedc865d1b 100644
--- a/app/assets/javascripts/extensions/jquery.js
+++ b/app/assets/javascripts/extensions/jquery.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, object-shorthand, comma-dangle, padded-blocks, max-len */
// Disable an element and add the 'disabled' Bootstrap class
(function() {
$.fn.extend({
diff --git a/app/assets/javascripts/extensions/object.js.es6 b/app/assets/javascripts/extensions/object.js.es6
new file mode 100644
index 00000000000..70a2d765abd
--- /dev/null
+++ b/app/assets/javascripts/extensions/object.js.es6
@@ -0,0 +1,26 @@
+/* eslint-disable no-restricted-syntax */
+
+// Adapted from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
+if (typeof Object.assign !== 'function') {
+ Object.assign = function assign(target, ...args) {
+ if (target == null) { // TypeError if undefined or null
+ throw new TypeError('Cannot convert undefined or null to object');
+ }
+
+ const to = Object(target);
+
+ for (let index = 0; index < args.length; index += 1) {
+ const nextSource = args[index];
+
+ if (nextSource != null) { // Skip over if undefined or null
+ for (const nextKey in nextSource) {
+ // Avoid bugs when hasOwnProperty is shadowed
+ if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
+ to[nextKey] = nextSource[nextKey];
+ }
+ }
+ }
+ }
+ return to;
+ };
+}
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index 732136f1f2c..785f2869970 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, padded-blocks, consistent-return */
+/* global FilesCommentButton */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 46e272c3311..804d7d9c4ab 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-param-reassign, quotes, quote-props, prefer-template, comma-dangle, padded-blocks, max-len */
(function() {
this.Flash = (function() {
var hideFlash;
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 824413bf20f..17d03c87bf5 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -1,22 +1,36 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, padded-blocks, vars-on-top, indent, no-extra-semi, no-multi-spaces, semi, max-len */
+
// Creates the variables for setting up GFM auto-completion
(function() {
- if (window.GitLab == null) {
- window.GitLab = {};
+ if (window.gl == null) {
+ window.gl = {};
+ }
+
+ function sanitize(str) {
+ return str.replace(/<(?:.|\n)*?>/gm, '');
}
- GitLab.GfmAutoComplete = {
- dataLoading: false,
- dataLoaded: false,
+ window.gl.GfmAutoComplete = {
+ dataSources: {},
+ defaultLoadingData: ['loading'],
cachedData: {},
- dataSource: '',
+ isLoadingData: {},
+ atTypeMap: {
+ ':': 'emojis',
+ '@': 'members',
+ '#': 'issues',
+ '!': 'mergeRequests',
+ '~': 'labels',
+ '%': 'milestones',
+ '/': 'commands'
+ },
// Emoji
Emoji: {
template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>'
},
// Team Members
Members: {
- template: '<li>${username} <small>${title}</small></li>'
+ template: '<li>${avatarTag} ${username} <small>${title}</small></li>'
},
Labels: {
template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
@@ -30,93 +44,103 @@
template: '<li>${title}</li>'
},
Loading: {
- template: '<li><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
+ template: '<li style="pointer-events: none;"><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
},
DefaultOptions: {
sorter: function(query, items, searchKey) {
- if ((items[0].name != null) && items[0].name === 'loading') {
+ if (gl.GfmAutoComplete.isLoading(items)) {
return items;
}
return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey);
},
filter: function(query, data, searchKey) {
- if (data[0] === 'loading') {
+ if (gl.GfmAutoComplete.isLoading(data)) {
+ gl.GfmAutoComplete.togglePreventSelection.call(this, true);
+ gl.GfmAutoComplete.fetchData(this.$inputor, this.at);
return data;
+ } else {
+ gl.GfmAutoComplete.togglePreventSelection.call(this, false);
+ return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
}
- return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
},
beforeInsert: function(value) {
- if (!GitLab.GfmAutoComplete.dataLoaded) {
- return this.at;
+ if (value && !this.setting.skipSpecialCharacterTest) {
+ var withoutAt = value.substring(1);
+ if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"';
+ }
+ return value;
+ },
+ matcher: function (flag, subtext) {
+ // The below is taken from At.js source
+ // Tweaked to commands to start without a space only if char before is a non-word character
+ // https://github.com/ichord/At.js
+ var _a, _y, regexp, match, atSymbols;
+ atSymbols = Object.keys(this.app.controllers).join('|');
+ subtext = subtext.split(' ').pop();
+ flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+
+ _a = decodeURI("%C3%80");
+ _y = decodeURI("%C3%BF");
+
+ regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?![" + atSymbols + "])([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi');
+
+ match = regexp.exec(subtext);
+
+ if (match) {
+ return match[2] || match[1];
} else {
- return value;
+ return null;
}
}
},
- setup: _.debounce(function(input) {
+ setup: function(input) {
// Add GFM auto-completion to all input fields, that accept GFM input.
this.input = input || $('.js-gfm-input');
- // destroy previous instances
- this.destroyAtWho();
- // set up instances
- this.setupAtWho();
-
- if (this.dataSource && !this.dataLoading && !this.cachedData) {
- this.dataLoading = true;
- return this.fetchData(this.dataSource)
- .done((data) => {
- this.dataLoading = false;
- this.loadData(data);
- });
- };
-
- if (this.cachedData != null) {
- return this.loadData(this.cachedData);
- }
- }, 1000),
- setupAtWho: function() {
+ this.setupLifecycle();
+ },
+ setupLifecycle() {
+ this.input.each((i, input) => {
+ const $input = $(input);
+ $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
+ });
+ },
+ setupAtWho: function($input) {
// Emoji
- this.input.atwho({
+ $input.atwho({
at: ':',
- displayTpl: (function(_this) {
- return function(value) {
- if (value.path != null) {
- return _this.Emoji.template;
- } else {
- return _this.Loading.template;
- }
- };
- })(this),
+ displayTpl: function(value) {
+ return value.path != null ? this.Emoji.template : this.Loading.template;
+ }.bind(this),
insertTpl: ':${name}:',
- data: ['loading'],
+ startWithSpace: false,
+ skipSpecialCharacterTest: true,
+ data: this.defaultLoadingData,
callbacks: {
sorter: this.DefaultOptions.sorter,
- filter: this.DefaultOptions.filter,
- beforeInsert: this.DefaultOptions.beforeInsert
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ filter: this.DefaultOptions.filter
}
});
// Team Members
- this.input.atwho({
+ $input.atwho({
at: '@',
- displayTpl: (function(_this) {
- return function(value) {
- if (value.username != null) {
- return _this.Members.template;
- } else {
- return _this.Loading.template;
- }
- };
- })(this),
+ displayTpl: function(value) {
+ return value.username != null ? this.Members.template : this.Loading.template;
+ }.bind(this),
insertTpl: '${atwho-at}${username}',
searchKey: 'search',
- data: ['loading'],
+ startWithSpace: false,
+ alwaysHighlightFirst: true,
+ skipSpecialCharacterTest: true,
+ data: this.defaultLoadingData,
callbacks: {
sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter,
beforeInsert: this.DefaultOptions.beforeInsert,
+ matcher: this.DefaultOptions.matcher,
beforeSave: function(members) {
return $.map(members, function(m) {
- var title;
+ let title = '';
if (m.username == null) {
return m;
}
@@ -124,34 +148,36 @@
if (m.count) {
title += " (" + m.count + ")";
}
+
+ const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase();
+ const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`;
+ const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`;
+
return {
username: m.username,
- title: gl.utils.sanitize(title),
- search: gl.utils.sanitize(m.username + " " + m.name)
+ avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
+ title: sanitize(title),
+ search: sanitize(m.username + " " + m.name)
};
});
}
}
});
- this.input.atwho({
+ $input.atwho({
at: '#',
alias: 'issues',
searchKey: 'search',
- displayTpl: (function(_this) {
- return function(value) {
- if (value.title != null) {
- return _this.Issues.template;
- } else {
- return _this.Loading.template;
- }
- };
- })(this),
- data: ['loading'],
+ displayTpl: function(value) {
+ return value.title != null ? this.Issues.template : this.Loading.template;
+ }.bind(this),
+ data: this.defaultLoadingData,
insertTpl: '${atwho-at}${id}',
+ startWithSpace: false,
callbacks: {
sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter,
beforeInsert: this.DefaultOptions.beforeInsert,
+ matcher: this.DefaultOptions.matcher,
beforeSave: function(issues) {
return $.map(issues, function(i) {
if (i.title == null) {
@@ -159,29 +185,28 @@
}
return {
id: i.iid,
- title: gl.utils.sanitize(i.title),
+ title: sanitize(i.title),
search: i.iid + " " + i.title
};
});
}
}
});
- this.input.atwho({
+ $input.atwho({
at: '%',
alias: 'milestones',
searchKey: 'search',
- displayTpl: (function(_this) {
- return function(value) {
- if (value.title != null) {
- return _this.Milestones.template;
- } else {
- return _this.Loading.template;
- }
- };
- })(this),
- insertTpl: '${atwho-at}"${title}"',
- data: ['loading'],
+ insertTpl: '${atwho-at}${title}',
+ displayTpl: function(value) {
+ return value.title != null ? this.Milestones.template : this.Loading.template;
+ }.bind(this),
+ startWithSpace: false,
+ data: this.defaultLoadingData,
callbacks: {
+ matcher: this.DefaultOptions.matcher,
+ sorter: this.DefaultOptions.sorter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ filter: this.DefaultOptions.filter,
beforeSave: function(milestones) {
return $.map(milestones, function(m) {
if (m.title == null) {
@@ -189,32 +214,28 @@
}
return {
id: m.iid,
- title: gl.utils.sanitize(m.title),
+ title: sanitize(m.title),
search: "" + m.title
};
});
}
}
});
- this.input.atwho({
+ $input.atwho({
at: '!',
alias: 'mergerequests',
searchKey: 'search',
- displayTpl: (function(_this) {
- return function(value) {
- if (value.title != null) {
- return _this.Issues.template;
- } else {
- return _this.Loading.template;
- }
- };
- })(this),
- data: ['loading'],
+ startWithSpace: false,
+ displayTpl: function(value) {
+ return value.title != null ? this.Issues.template : this.Loading.template;
+ }.bind(this),
+ data: this.defaultLoadingData,
insertTpl: '${atwho-at}${id}',
callbacks: {
sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter,
beforeInsert: this.DefaultOptions.beforeInsert,
+ matcher: this.DefaultOptions.matcher,
beforeSave: function(merges) {
return $.map(merges, function(m) {
if (m.title == null) {
@@ -222,32 +243,41 @@
}
return {
id: m.iid,
- title: gl.utils.sanitize(m.title),
+ title: sanitize(m.title),
search: m.iid + " " + m.title
};
});
}
}
});
- this.input.atwho({
+ $input.atwho({
at: '~',
alias: 'labels',
searchKey: 'search',
- displayTpl: this.Labels.template,
+ data: this.defaultLoadingData,
+ displayTpl: function(value) {
+ return this.isLoading(value) ? this.Loading.template : this.Labels.template;
+ }.bind(this),
insertTpl: '${atwho-at}${title}',
+ startWithSpace: false,
callbacks: {
+ matcher: this.DefaultOptions.matcher,
+ sorter: this.DefaultOptions.sorter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ filter: this.DefaultOptions.filter,
beforeSave: function(merges) {
+ if (gl.GfmAutoComplete.isLoading(merges)) return merges;
var sanitizeLabelTitle;
sanitizeLabelTitle = function(title) {
if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) {
- return "\"" + (gl.utils.sanitize(title)) + "\"";
+ return "\"" + (sanitize(title)) + "\"";
} else {
- return gl.utils.sanitize(title);
+ return sanitize(title);
}
};
return $.map(merges, function(m) {
return {
- title: sanitizeLabelTitle(m.title),
+ title: sanitize(m.title),
color: m.color,
search: "" + m.title
};
@@ -256,11 +286,14 @@
}
});
// We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
- this.input.filter('[data-supports-slash-commands="true"]').atwho({
+ $input.filter('[data-supports-slash-commands="true"]').atwho({
at: '/',
alias: 'commands',
searchKey: 'search',
+ skipSpecialCharacterTest: true,
+ data: this.defaultLoadingData,
displayTpl: function(value) {
+ if (this.isLoading(value)) return this.Loading.template;
var tpl = '<li>/${name}';
if (value.aliases.length > 0) {
tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
@@ -273,7 +306,7 @@
}
tpl += '</li>';
return _.template(tpl)(value);
- },
+ }.bind(this),
insertTpl: function(value) {
var tpl = "/${name} ";
var reference_prefix = null;
@@ -291,6 +324,7 @@
filter: this.DefaultOptions.filter,
beforeInsert: this.DefaultOptions.beforeInsert,
beforeSave: function(commands) {
+ if (gl.GfmAutoComplete.isLoading(commands)) return commands;
return $.map(commands, function(c) {
var search = c.name;
if (c.aliases.length > 0) {
@@ -318,32 +352,40 @@
});
return;
},
- destroyAtWho: function() {
- return this.input.atwho('destroy');
- },
- fetchData: function(dataSource) {
- return $.getJSON(dataSource);
+ fetchData: function($input, at) {
+ if (this.isLoadingData[at]) return;
+ this.isLoadingData[at] = true;
+ if (this.cachedData[at]) {
+ this.loadData($input, at, this.cachedData[at]);
+ } else {
+ $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => {
+ this.loadData($input, at, data);
+ }).fail(() => { this.isLoadingData[at] = false; });
+ }
},
- loadData: function(data) {
- this.cachedData = data;
- this.dataLoaded = true;
- // load members
- this.input.atwho('load', '@', data.members);
- // load issues
- this.input.atwho('load', 'issues', data.issues);
- // load milestones
- this.input.atwho('load', 'milestones', data.milestones);
- // load merge requests
- this.input.atwho('load', 'mergerequests', data.mergerequests);
- // load emojis
- this.input.atwho('load', ':', data.emojis);
- // load labels
- this.input.atwho('load', '~', data.labels);
- // load commands
- this.input.atwho('load', '/', data.commands);
+ loadData: function($input, at, data) {
+ this.isLoadingData[at] = false;
+ this.cachedData[at] = data;
+ $input.atwho('load', at, data);
// This trigger at.js again
// otherwise we would be stuck with loading until the user types
- return $(':focus').trigger('keyup');
+ return $input.trigger('keyup');
+ },
+ isLoading(data) {
+ if (!data) return false;
+ if (Array.isArray(data)) data = data[0];
+ return data === this.defaultLoadingData[0] || data.name === this.defaultLoadingData[0];
+ },
+ togglePreventSelection(isPrevented = !!this.setting.tabSelectsMatch) {
+ this.setting.tabSelectsMatch = !isPrevented;
+ this.setting.spaceSelectsMatch = !isPrevented;
+ const eventListenerAction = `${isPrevented ? 'add' : 'remove'}EventListener`;
+ this.$inputor[0][eventListenerAction]('keydown', gl.GfmAutoComplete.preventSpaceTabEnter);
+ },
+ preventSpaceTabEnter(e) {
+ const key = e.which || e.keyCode;
+ const preventables = [9, 13, 32];
+ if (preventables.indexOf(key) > -1) e.preventDefault();
}
};
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 98e43c4d088..57dabfe05e4 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, space-before-blocks, prefer-rest-params, max-len, vars-on-top, no-plusplus, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, semi, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, padded-blocks, prefer-template, no-param-reassign, no-loop-func, no-extra-semi, keyword-spacing, no-mixed-operators */
+/* global fuzzaldrinPlus */
+/* global Turbolinks */
+
(function() {
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote,
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
@@ -20,7 +23,6 @@
this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
$inputContainer = this.input.parent();
$clearButton = $inputContainer.find('.js-dropdown-input-clear');
- this.indeterminateIds = [];
$clearButton.on('click', (function(_this) {
// Clear click
return function(e) {
@@ -188,7 +190,7 @@
})();
GitLabDropdown = (function() {
- var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, currentIndex;
+ var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex;
LOADING_CLASS = "is-loading";
@@ -249,7 +251,7 @@
_this.fullData = data;
_this.parseData(_this.fullData);
_this.focusTextInput();
- if (_this.options.filterable && _this.filter && _this.filter.input) {
+ if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val().trim() !== '') {
return _this.filter.input.trigger('input');
}
};
@@ -345,12 +347,12 @@
$el = $(this);
selected = self.rowClicked($el);
if (self.options.clicked) {
- self.options.clicked(selected, $el, e);
+ self.options.clicked(selected[0], $el, e, selected[1]);
}
// Update label right after all modifications in dropdown has been done
if (self.options.toggleLabel) {
- self.updateLabel(selected, $el, self);
+ self.updateLabel(selected[0], $el, self);
}
$el.trigger('blur');
@@ -441,12 +443,6 @@
this.resetRows();
this.addArrowKeyEvent();
- if (this.options.setIndeterminateIds) {
- this.options.setIndeterminateIds.call(this);
- }
- if (this.options.setActiveIds) {
- this.options.setActiveIds.call(this);
- }
// Makes indeterminate items effective
if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
this.parseData(this.fullData);
@@ -480,11 +476,6 @@
if (this.options.filterable) {
$input.blur().val("");
}
- // Triggering 'keyup' will re-render the dropdown which is not always required
- // specially if we want to keep the state of the dropdown needed for bulk-assignment
- if (!this.options.persistWhenHide) {
- $input.trigger("input");
- }
if (this.dropdown.find(".dropdown-toggle-page").length) {
$('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
}
@@ -617,7 +608,8 @@
};
GitLabDropdown.prototype.rowClicked = function(el) {
- var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value;
+ var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
+
fieldName = this.options.fieldName;
isInput = $(this.el).is('input');
if (this.renderedData) {
@@ -638,7 +630,7 @@
el.addClass(ACTIVE_CLASS);
}
- return selectedObject;
+ return [selectedObject];
}
field = [];
@@ -650,7 +642,13 @@
} else if(value) {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
}
+
+ if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
+ return;
+ }
+
if (el.hasClass(ACTIVE_CLASS)) {
+ isMarking = false;
el.removeClass(ACTIVE_CLASS);
if (field && field.length) {
if (isInput) {
@@ -660,6 +658,7 @@
}
}
} else if (el.hasClass(INDETERMINATE_CLASS)) {
+ isMarking = true;
el.addClass(ACTIVE_CLASS);
el.removeClass(INDETERMINATE_CLASS);
if (field && field.length && value == null) {
@@ -669,6 +668,7 @@
this.addInput(fieldName, value, selectedObject);
}
} else {
+ isMarking = true;
if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
if (!isInput) {
@@ -689,7 +689,7 @@
}
}
- return selectedObject;
+ return [selectedObject, isMarking];
};
GitLabDropdown.prototype.focusTextInput = function() {
diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6
index 6ce392d2a5b..63f9cafa8d0 100644
--- a/app/assets/javascripts/gl_field_errors.js.es6
+++ b/app/assets/javascripts/gl_field_errors.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign, padded-blocks */
//= require gl_field_error
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index ce54c34492d..7dc2d13e5d8 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, padded-blocks, max-len */
+/* global GitLab */
+/* global DropzoneInput */
+/* global autosize */
+
(function() {
this.GLForm = (function() {
function GLForm(form) {
@@ -26,7 +30,7 @@
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'));
- GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
+ gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
new DropzoneInput(this.form);
autosize(this.textarea);
// form and textarea event listeners
diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js
index e103748d499..32c26349da0 100644
--- a/app/assets/javascripts/graphs/graphs_bundle.js
+++ b/app/assets/javascripts/graphs/graphs_bundle.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
// This is a manifest file that'll be compiled into including all the files listed below.
// Add new JavaScript code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js
index b796a9abb49..3273bf3a263 100644
--- a/app/assets/javascripts/graphs/stat_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-return-assign, padded-blocks, max-len */
(function() {
this.StatGraph = (function() {
function StatGraph() {}
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
index 818bff0c413..2d08a7c6ac3 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js
@@ -1,4 +1,9 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, padded-blocks */
+/* global ContributorsGraph */
+/* global ContributorsAuthorGraph */
+/* global ContributorsMasterGraph */
+/* global ContributorsStatGraphUtil */
+/* global d3 */
/*= require d3 */
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
index dea26a3f1e1..9c5e9381e52 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, padded-blocks, newline-per-chained-call, no-else-return */
+/* global d3 */
+/* global ContributorsGraph */
/*= require d3 */
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
index 362a77e868f..1982f4af939 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
@@ -1,8 +1,8 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, no-plusplus, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, padded-blocks, max-len */
(function() {
window.ContributorsStatGraphUtil = {
parse_log: function(log) {
- var by_author, by_email, data, entry, i, len, total;
+ var by_author, by_email, data, entry, i, len, total, normalized_email;
total = {};
by_author = {};
by_email = {};
@@ -11,7 +11,8 @@
if (total[entry.date] == null) {
this.add_date(entry.date, total);
}
- data = by_author[entry.author_name] || by_email[entry.author_email];
+ normalized_email = entry.author_email.toLowerCase();
+ data = by_author[entry.author_name] || by_email[normalized_email];
if (data == null) {
data = this.add_author(entry, by_author, by_email);
}
@@ -32,12 +33,14 @@
return collection[date].date = date;
},
add_author: function(author, by_author, by_email) {
- var data;
+ var data, normalized_email;
data = {};
data.author_name = author.author_name;
data.author_email = author.author_email;
+ normalized_email = author.author_email.toLowerCase();
by_author[author.author_name] = data;
- return by_email[author.author_email] = data;
+ by_email[normalized_email] = data;
+ return data;
},
store_data: function(entry, total, by_author) {
this.store_commits(total, by_author);
diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js
index 774477dc7a9..17a76168a79 100644
--- a/app/assets/javascripts/group_avatar.js
+++ b/app/assets/javascripts/group_avatar.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, padded-blocks, max-len */
(function() {
this.GroupAvatar = (function() {
function GroupAvatar() {
diff --git a/app/assets/javascripts/group_label_subscription.js.es6 b/app/assets/javascripts/group_label_subscription.js.es6
new file mode 100644
index 00000000000..8e10e424412
--- /dev/null
+++ b/app/assets/javascripts/group_label_subscription.js.es6
@@ -0,0 +1,54 @@
+/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, padded-blocks, max-len */
+
+(function(global) {
+ class GroupLabelSubscription {
+ constructor(container) {
+ const $container = $(container);
+ this.$dropdown = $container.find('.dropdown');
+ this.$subscribeButtons = $container.find('.js-subscribe-button');
+ this.$unsubscribeButtons = $container.find('.js-unsubscribe-button');
+
+ this.$subscribeButtons.on('click', this.subscribe.bind(this));
+ this.$unsubscribeButtons.on('click', this.unsubscribe.bind(this));
+ }
+
+ unsubscribe(event) {
+ event.preventDefault();
+
+ const url = this.$unsubscribeButtons.attr('data-url');
+
+ $.ajax({
+ type: 'POST',
+ url: url
+ }).done(() => {
+ this.toggleSubscriptionButtons();
+ this.$unsubscribeButtons.removeAttr('data-url');
+ });
+ }
+
+ subscribe(event) {
+ event.preventDefault();
+
+ const $btn = $(event.currentTarget);
+ const url = $btn.attr('data-url');
+
+ this.$unsubscribeButtons.attr('data-url', url);
+
+ $.ajax({
+ type: 'POST',
+ url: url
+ }).done(() => {
+ this.toggleSubscriptionButtons();
+ });
+ }
+
+ toggleSubscriptionButtons() {
+ this.$dropdown.toggleClass('hidden');
+ this.$subscribeButtons.toggleClass('hidden');
+ this.$unsubscribeButtons.toggleClass('hidden');
+ }
+ }
+
+ global.GroupLabelSubscription = GroupLabelSubscription;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index e3c39c895ba..99700e7562a 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, padded-blocks, max-len */
+/* global Api */
+
(function() {
var slice = [].slice;
@@ -14,7 +16,7 @@
multiple: $(select).hasClass('multiselect'),
minimumInputLength: 0,
query: function(query) {
- options = { all_available: all_available, skip_groups: skip_groups };
+ var options = { all_available: all_available, skip_groups: skip_groups };
return Api.groups(query.term, options, function(groups) {
var data;
data = {
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 81fcaf06430..c7cbf9ca44b 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, padded-blocks, prefer-arrow-callback, no-var, max-len */
(function() {
$(document).on('todo:toggle', function(e, count) {
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index c53f7c88aa2..fa795be07ed 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, padded-blocks, vars-on-top, no-new, max-len */
+
(function() {
- this.ImporterStatus = (function() {
+ window.ImporterStatus = (function() {
function ImporterStatus(jobs_url, import_url) {
this.jobs_url = jobs_url;
this.import_url = import_url;
@@ -75,7 +76,7 @@
var jobsImportPath = $('.js-importer-status').data('jobs-import-path');
var importPath = $('.js-importer-status').data('import-path');
- new ImporterStatus(jobsImportPath, importPath);
+ new window.ImporterStatus(jobsImportPath, importPath);
}
});
}).call(this);
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
index 46503c290ae..1c10a7445bb 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, prefer-const, padded-blocks, wrap-iife, max-len */
+/* global Issuable */
+/* global Turbolinks */
+
(function() {
var issuable_created;
@@ -141,6 +144,9 @@
const $issuesOtherFilters = $('.issues-other-filters');
const $issuesBulkUpdate = $('.issues_bulk_update');
+ this.issuableBulkActions.willUpdateLabels = false;
+ this.issuableBulkActions.setOriginalDropdownData();
+
if ($checkedIssues.length > 0) {
let ids = $.map($checkedIssues, function(value) {
return $(value).data('id');
@@ -152,7 +158,6 @@
$updateIssuesIds.val([]);
$issuesBulkUpdate.hide();
$issuesOtherFilters.show();
- this.issuableBulkActions.willUpdateLabels = false;
}
return true;
},
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index fae49ee6144..4aaad111082 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, padded-blocks, max-len */
+/* global UsersSelect */
+
(function() {
this.IssuableContext = (function() {
function IssuableContext(currentUser) {
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 849b45756ee..1c4086517fe 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -1,4 +1,9 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, radix, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, padded-blocks, max-len */
+/* global GitLab */
+/* global UsersSelect */
+/* global ZenMode */
+/* global Autosave */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -14,7 +19,7 @@
this.renderWipExplanation = bind(this.renderWipExplanation, this);
this.resetAutosave = bind(this.resetAutosave, this);
this.handleSubmit = bind(this.handleSubmit, this);
- GitLab.GfmAutoComplete.setup();
+ gl.GfmAutoComplete.setup();
new UsersSelect();
new ZenMode();
this.titleField = this.form.find("input[name*='[title]']");
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 67ace697936..63b70d4be17 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, padded-blocks, max-len */
+/* global Flash */
/*= require flash */
/*= require jquery.waitforimages */
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
index d7262e5eb74..b39d8274e13 100644
--- a/app/assets/javascripts/issue_status_select.js
+++ b/app/assets/javascripts/issue_status_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */
(function() {
this.IssueStatusSelect = (function() {
function IssueStatusSelect() {
diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6
index 9697fb33566..52fd5d71b18 100644
--- a/app/assets/javascripts/issues_bulk_assignment.js.es6
+++ b/app/assets/javascripts/issues_bulk_assignment.js.es6
@@ -1,10 +1,14 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, radix, max-len, padded-blocks, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
+/* global Issuable */
+/* global Flash */
+
((global) => {
class IssuableBulkActions {
- constructor({ container, form, issues } = {}) {
- this.container = container || $('.content'),
+ constructor({ container, form, issues, prefixId } = {}) {
+ this.prefixId = prefixId || 'issue_';
this.form = form || this.getElement('.bulk-update');
+ this.$labelDropdown = this.form.find('.js-label-select');
this.issues = issues || this.getElement('.issues-list .issue');
this.form.data('bulkActions', this);
this.willUpdateLabels = false;
@@ -13,10 +17,6 @@
Issuable.initChecks();
}
- getElement(selector) {
- return this.container.find(selector);
- }
-
bindEvents() {
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
}
@@ -70,10 +70,7 @@
getUnmarkedIndeterminedLabels() {
const result = [];
- const labelsToKeep = [];
-
- this.getElement('.labels-filter .is-indeterminate')
- .each((i, el) => labelsToKeep.push($(el).data('labelId')));
+ const labelsToKeep = this.$labelDropdown.data('indeterminate');
this.getLabelsFromSelection().forEach((id) => {
if (labelsToKeep.indexOf(id) === -1) {
@@ -103,45 +100,65 @@
}
};
if (this.willUpdateLabels) {
- this.getLabelsToApply().map(function(id) {
- return formData.update.add_label_ids.push(id);
- });
- this.getLabelsToRemove().map(function(id) {
- return formData.update.remove_label_ids.push(id);
- });
+ formData.update.add_label_ids = this.$labelDropdown.data('marked');
+ formData.update.remove_label_ids = this.$labelDropdown.data('unmarked');
}
return formData;
}
- getLabelsToApply() {
+ setOriginalDropdownData() {
+ const $labelSelect = $('.bulk-update .js-label-select');
+ $labelSelect.data('common', this.getOriginalCommonIds());
+ $labelSelect.data('marked', this.getOriginalMarkedIds());
+ $labelSelect.data('indeterminate', this.getOriginalIndeterminateIds());
+ }
+
+ // From issuable's initial bulk selection
+ getOriginalCommonIds() {
const labelIds = [];
- const $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
- $labels.each(function(k, label) {
- if (label) {
- return labelIds.push(parseInt($(label).val()));
- }
+
+ this.getElement('.selected_issue:checked').each((i, el) => {
+ labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
- return labelIds;
+ return _.intersection.apply(this, labelIds);
}
+ // From issuable's initial bulk selection
+ getOriginalMarkedIds() {
+ const labelIds = [];
+ this.getElement('.selected_issue:checked').each((i, el) => {
+ labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
+ });
+ return _.intersection.apply(this, labelIds);
+ }
- /**
- * Returns Label IDs that will be removed from issue selection
- * @return {Array} Array of labels IDs
- */
-
- getLabelsToRemove() {
- const result = [];
- const indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
- const labelsToApply = this.getLabelsToApply();
- indeterminatedLabels.map(function(id) {
- // We need to exclude label IDs that will be applied
- // By not doing this will cause issues from selection to not add labels at all
- if (labelsToApply.indexOf(id) === -1) {
- return result.push(id);
- }
+ // From issuable's initial bulk selection
+ getOriginalIndeterminateIds() {
+ const uniqueIds = [];
+ const labelIds = [];
+ let issuableLabels = [];
+
+ // Collect unique label IDs for all checked issues
+ this.getElement('.selected_issue:checked').each((i, el) => {
+ issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
+ issuableLabels.forEach((labelId) => {
+ // Store unique IDs
+ if (uniqueIds.indexOf(labelId) === -1) {
+ uniqueIds.push(labelId);
+ }
+ });
+ // Store array of IDs per issuable
+ labelIds.push(issuableLabels);
});
- return result;
+ // Add uniqueIds to add it as argument for _.intersection
+ labelIds.unshift(uniqueIds);
+ // Return IDs that are present but not in all selected issueables
+ return _.difference(uniqueIds, _.intersection.apply(this, labelIds));
+ }
+
+ getElement(selector) {
+ this.scopeEl = this.scopeEl || $('.content');
+ return this.scopeEl.find(selector);
}
}
diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6
index 175623e7448..33c5e35324d 100644
--- a/app/assets/javascripts/label_manager.js.es6
+++ b/app/assets/javascripts/label_manager.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, padded-blocks, max-len */
+/* global Flash */
+
((global) => {
class LabelManager {
@@ -104,4 +106,3 @@
gl.LabelManager = LabelManager;
})(window.gl || (window.gl = {}));
-
diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js
index 3033e8ca5c2..10de13c9a8a 100644
--- a/app/assets/javascripts/labels.js
+++ b/app/assets/javascripts/labels.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, vars-on-top, no-unused-vars, padded-blocks, max-len */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index c334e3e0c02..ec2fc87bece 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -1,12 +1,16 @@
-/* eslint-disable */
+/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks */
+/* global Issuable */
+/* global ListLabel */
+
(function() {
this.LabelsSelect = (function() {
function LabelsSelect() {
var _this;
_this = this;
$('.js-label-select').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;
+ 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');
$toggleText = $dropdown.find('.dropdown-toggle-text');
namespacePath = $dropdown.data('namespace-path');
projectPath = $dropdown.data('project-path');
@@ -122,7 +126,7 @@
});
});
};
- return $dropdown.glDropdown({
+ $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) {
return $.ajax({
@@ -169,33 +173,40 @@
});
},
renderRow: function(label, instance) {
- var $a, $li, active, color, colorEl, indeterminate, removesAll, selectedClass, spacing;
+ var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue;
$li = $('<li>');
$a = $('<a href="#">');
selectedClass = [];
removesAll = label.id <= 0 || (label.id == null);
if ($dropdown.hasClass('js-filter-bulk-update')) {
- indeterminate = instance.indeterminateIds;
- active = instance.activeIds;
+ indeterminate = $dropdown.data('indeterminate') || [];
+ marked = $dropdown.data('marked') || [];
+
if (indeterminate.indexOf(label.id) !== -1) {
selectedClass.push('is-indeterminate');
}
- if (active.indexOf(label.id) !== -1) {
+
+ if (marked.indexOf(label.id) !== -1) {
// Remove is-indeterminate class if the item will be marked as active
i = selectedClass.indexOf('is-indeterminate');
if (i !== -1) {
selectedClass.splice(i, 1);
}
selectedClass.push('is-active');
- // Add input manually
- instance.addInput(this.fieldName, label.id);
}
- }
- if (this.id(label) && $form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + this.id(label).toString().replace(/'/g, '\\\'') + "']").length) {
- selectedClass.push('is-active');
- }
- if ($dropdown.hasClass('js-multiselect') && removesAll) {
- selectedClass.push('dropdown-clear-active');
+ } else {
+ if (this.id(label)) {
+ dropdownName = $dropdown.data('fieldName');
+ dropdownValue = this.id(label).toString().replace(/'/g, '\\\'');
+
+ if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) {
+ selectedClass.push('is-active');
+ }
+ }
+
+ if ($dropdown.hasClass('js-multiselect') && removesAll) {
+ selectedClass.push('dropdown-clear-active');
+ }
}
if (label.duplicate) {
spacing = 100 / label.color.length;
@@ -231,7 +242,6 @@
// Return generated html
return $li.html($a).prop('outerHTML');
},
- persistWhenHide: $dropdown.data('persistWhenHide'),
search: {
fields: ['title']
},
@@ -310,18 +320,15 @@
}
}
}
- if ($dropdown.hasClass('js-filter-bulk-update')) {
- // If we are persisting state we need the classes
- if (!this.options.persistWhenHide) {
- return $dropdown.parent().find('.is-active, .is-indeterminate').removeClass();
- }
- }
},
multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'),
- clicked: function(label, $el, e) {
+ clicked: function(label, $el, e, isMarking) {
var isIssueIndex, isMRIndex, page;
- _this.enableBulkLabelDropdown();
+
+ page = $('body').data('page');
+ isIssueIndex = page === 'projects:issues:index';
+ isMRIndex = page === 'projects:merge_requests:index';
if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
$dropdown.parent()
@@ -330,12 +337,11 @@
}
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
+ _this.enableBulkLabelDropdown();
+ _this.setDropdownData($dropdown, isMarking, this.id(label));
return;
}
- page = $('body').data('page');
- isIssueIndex = page === 'projects:issues:index';
- isMRIndex = page === 'projects:merge_requests:index';
if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
if (label.isAny) {
gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
@@ -397,17 +403,10 @@
}
}
},
- setIndeterminateIds: function() {
- if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
- return this.indeterminateIds = _this.getIndeterminateIds();
- }
- },
- setActiveIds: function() {
- if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
- return this.activeIds = _this.getActiveIds();
- }
- }
});
+
+ // Set dropdown data
+ _this.setOriginalDropdownData($dropdownContainer, $dropdown);
});
this.bindEvents();
}
@@ -420,34 +419,9 @@
if ($('.selected_issue:checked').length) {
return;
}
- // Remove inputs
- $('.issues_bulk_update .labels-filter input[type="hidden"]').remove();
- // Also restore button text
return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label');
};
- LabelsSelect.prototype.getIndeterminateIds = function() {
- var label_ids;
- label_ids = [];
- $('.selected_issue:checked').each(function(i, el) {
- var issue_id;
- issue_id = $(el).data('id');
- return label_ids.push($("#issue_" + issue_id).data('labels'));
- });
- return _.flatten(label_ids);
- };
-
- LabelsSelect.prototype.getActiveIds = function() {
- var label_ids;
- label_ids = [];
- $('.selected_issue:checked').each(function(i, el) {
- var issue_id;
- issue_id = $(el).data('id');
- return label_ids.push($("#issue_" + issue_id).data('labels'));
- });
- return _.intersection.apply(_, label_ids);
- };
-
LabelsSelect.prototype.enableBulkLabelDropdown = function() {
var issuableBulkActions;
if ($('.selected_issue:checked').length) {
@@ -456,8 +430,59 @@
}
};
- return LabelsSelect;
+ LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) {
+ var i, markedIds, unmarkedIds, indeterminateIds;
+ var issuableBulkActions = $('.bulk-update').data('bulkActions');
+ markedIds = $dropdown.data('marked') || [];
+ unmarkedIds = $dropdown.data('unmarked') || [];
+ indeterminateIds = $dropdown.data('indeterminate') || [];
+
+ if (isMarking) {
+ markedIds.push(value);
+
+ i = indeterminateIds.indexOf(value);
+ if (i > -1) {
+ indeterminateIds.splice(i, 1);
+ }
+
+ i = unmarkedIds.indexOf(value);
+ if (i > -1) {
+ unmarkedIds.splice(i, 1);
+ }
+ } else {
+ // If marked item (not common) is unmarked
+ i = markedIds.indexOf(value);
+ if (i > -1) {
+ markedIds.splice(i, 1);
+ }
+
+ // If an indeterminate item is being unmarked
+ if (issuableBulkActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
+ unmarkedIds.push(value);
+ }
+
+ // If a marked item is being unmarked
+ // (a marked item could also be a label that is present in all selection)
+ if (issuableBulkActions.getOriginalCommonIds().indexOf(value) > -1) {
+ unmarkedIds.push(value);
+ }
+ }
+
+ $dropdown.data('marked', markedIds);
+ $dropdown.data('unmarked', unmarkedIds);
+ $dropdown.data('indeterminate', indeterminateIds);
+ };
+
+ LabelsSelect.prototype.setOriginalDropdownData = function($container, $dropdown) {
+ var labels = [];
+ $container.find('[name="label_name[]"]').map(function() {
+ return labels.push(this.value);
+ });
+ $dropdown.data('marked', labels);
+ };
+
+ return LabelsSelect;
})();
}).call(this);
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 6b4edf02f4d..2b700539c2b 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, indent, vars-on-top, padded-blocks, max-len */
(function() {
var hideEndFade;
diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js
index b1718e89d3d..4cdf99cae72 100644
--- a/app/assets/javascripts/lib/ace.js
+++ b/app/assets/javascripts/lib/ace.js
@@ -1,3 +1,2 @@
-/* eslint-disable */
/*= require ace-rails-ap */
/*= require ace/ext-searchbox */
diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js
index e1dfdae97de..d8ad5aaeffe 100644
--- a/app/assets/javascripts/lib/chart.js
+++ b/app/assets/javascripts/lib/chart.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
/*= require Chart */
diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js
index 155e30cc462..5221f85ba7a 100644
--- a/app/assets/javascripts/lib/cropper.js
+++ b/app/assets/javascripts/lib/cropper.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
/*= require cropper */
diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js
index 0c9c2787077..57e7986576c 100644
--- a/app/assets/javascripts/lib/d3.js
+++ b/app/assets/javascripts/lib/d3.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
/*= require d3 */
diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js
index cc445db274b..5a9a501efe3 100644
--- a/app/assets/javascripts/lib/raphael.js
+++ b/app/assets/javascripts/lib/raphael.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
/*= require raphael */
/*= require g.raphael */
diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js
index a68edab2aad..83957af94f3 100644
--- a/app/assets/javascripts/lib/utils/animate.js
+++ b/app/assets/javascripts/lib/utils/animate.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, no-void, prefer-template, no-var, new-cap, prefer-arrow-callback, consistent-return, padded-blocks, max-len */
(function() {
(function(w) {
if (w.gl == null) {
diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6
new file mode 100644
index 00000000000..e810ee85bd3
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6
@@ -0,0 +1,113 @@
+/**
+ * Linked Tabs
+ *
+ * Handles persisting and restores the current tab selection and content.
+ * Reusable component for static content.
+ *
+ * ### Example Markup
+ *
+ * <ul class="nav-links tab-links">
+ * <li class="active">
+ * <a data-action="tab1" data-target="#tab1" data-toggle="tab" href="/path/tab1">
+ * Tab 1
+ * </a>
+ * </li>
+ * <li class="groups-tab">
+ * <a data-action="tab2" data-target="#tab2" data-toggle="tab" href="/path/tab2">
+ * Tab 2
+ * </a>
+ * </li>
+ *
+ *
+ * <div class="tab-content">
+ * <div class="tab-pane" id="tab1">
+ * Tab 1 Content
+ * </div>
+ * <div class="tab-pane" id="tab2">
+ * Tab 2 Content
+ * </div>
+ * </div>
+ *
+ *
+ * ### How to use
+ *
+ * new window.gl.LinkedTabs({
+ * action: "#{controller.action_name}",
+ * defaultAction: 'tab1',
+ * parentEl: '.tab-links'
+ * });
+ */
+
+(() => {
+ window.gl = window.gl || {};
+
+ window.gl.LinkedTabs = class LinkedTabs {
+ /**
+ * Binds the events and activates de default tab.
+ *
+ * @param {Object} options
+ */
+ constructor(options) {
+ this.options = options || {};
+
+ this.defaultAction = this.options.defaultAction;
+ this.action = this.options.action || this.defaultAction;
+
+ if (this.action === 'show') {
+ this.action = this.defaultAction;
+ }
+
+ this.currentLocation = window.location;
+
+ const tabSelector = `${this.options.parentEl} a[data-toggle="tab"]`;
+
+ // since this is a custom event we need jQuery :(
+ $(document)
+ .off('shown.bs.tab', tabSelector)
+ .on('shown.bs.tab', tabSelector, e => this.tabShown(e));
+
+ this.activateTab(this.action);
+ }
+
+ /**
+ * Handles the `shown.bs.tab` event to set the currect url action.
+ *
+ * @param {type} evt
+ * @return {Function}
+ */
+ tabShown(evt) {
+ const source = evt.target.getAttribute('href');
+
+ return this.setCurrentAction(source);
+ }
+
+ /**
+ * Updates the URL with the path that matched the given action.
+ *
+ * @param {String} source
+ * @return {String}
+ */
+ setCurrentAction(source) {
+ const copySource = source;
+
+ copySource.replace(/\/+$/, '');
+
+ const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
+
+ history.replaceState({
+ turbolinks: true,
+ url: newState,
+ }, document.title, newState);
+ return newState;
+ }
+
+ /**
+ * Given the current action activates the correct tab.
+ * http://getbootstrap.com/javascript/#tab-show
+ * Note: Will trigger `shown.bs.tab`
+ */
+ activateTab() {
+ return $(`${this.options.parentEl} a[data-action='${this.action}']`).tab('show');
+ }
+ };
+})();
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 8447421195d..8fa80502d92 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, padded-blocks, max-len, prefer-template */
(function() {
(function(w) {
var base;
@@ -33,10 +33,6 @@
});
};
- w.gl.utils.split = function(val) {
- return val.split(/,\s*/);
- };
-
w.gl.utils.extractLast = function(term) {
return this.split(term).pop();
};
@@ -67,83 +63,57 @@
});
};
- w.gl.utils.disableButtonIfAnyEmptyField = function(form, form_selector, button_selector) {
- var closest_submit, updateButtons;
- closest_submit = form.find(button_selector);
- updateButtons = function() {
- var filled;
- filled = true;
- form.find('input').filter(form_selector).each(function() {
- return filled = this.rstrip($(this).val()) !== "" || !$(this).attr('required');
- });
- if (filled) {
- return closest_submit.enable();
- } else {
- return closest_submit.disable();
- }
- };
- updateButtons();
- return form.keyup(updateButtons);
- };
-
- w.gl.utils.sanitize = function(str) {
- return str.replace(/<(?:.|\n)*?>/gm, '');
- };
-
- w.gl.utils.unbindEvents = function() {
- return $(document).off('scroll');
- };
+ // automatically adjust scroll position for hash urls taking the height of the navbar into account
+ // https://github.com/twitter/bootstrap/issues/1768
+ w.gl.utils.handleLocationHash = function() {
+ var hash = w.gl.utils.getLocationHash();
+ if (!hash) return;
- w.gl.utils.shiftWindow = function() {
- return w.scrollBy(0, -100);
- };
+ 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;
- gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) {
- return $tooltipEl.tooltip('destroy').attr('title', newTitle).tooltip('fixTitle');
- };
- gl.utils.preventDisabledButtons = function() {
- return $('.btn').click(function(e) {
- if ($(this).hasClass('disabled')) {
- e.preventDefault();
- e.stopImmediatePropagation();
- return false;
+ // 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
+ if (fixedTabs) {
+ adjustment -= fixedTabs.offsetHeight;
+ }
+ window.scrollBy(0, adjustment);
+ }
};
+
gl.utils.getPagePath = function() {
return $('body').data('page').split(':')[0];
};
+
gl.utils.parseUrl = function (url) {
var parser = document.createElement('a');
parser.href = url;
return parser;
};
- gl.utils.cleanupBeforeFetch = function() {
- // Unbind scroll events
- $(document).off('scroll');
- // Close any open tooltips
- $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
+ gl.utils.parseUrlPathname = function (url) {
+ var parsedUrl = gl.utils.parseUrl(url);
+ // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11
+ // We have to make sure we always have an absolute path.
+ return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname;
};
- return jQuery.timefor = function(time, suffix, expiredLabel) {
- var suffixFromNow, timefor;
- if (!time) {
- return '';
- }
- suffix || (suffix = 'remaining');
- expiredLabel || (expiredLabel = 'Past due');
- jQuery.timeago.settings.allowFuture = true;
- suffixFromNow = jQuery.timeago.settings.strings.suffixFromNow;
- jQuery.timeago.settings.strings.suffixFromNow = suffix;
- timefor = $.timeago(time);
- if (timefor.indexOf('ago') > -1) {
- timefor = expiredLabel;
- }
- jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow;
- return timefor;
+ gl.utils.isMetaKey = function(e) {
+ return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
};
+
})(window);
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/custom_event_polyfill.js.es6 b/app/assets/javascripts/lib/utils/custom_event_polyfill.js.es6
new file mode 100644
index 00000000000..5ae978010c9
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/custom_event_polyfill.js.es6
@@ -0,0 +1,12 @@
+/**
+ * CustomEvent support for IE
+ */
+if (typeof window.CustomEvent !== 'function') {
+ window.CustomEvent = function CustomEvent(e, params) {
+ const options = params || { bubbles: false, cancelable: false, detail: undefined };
+ const evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent(e, options.bubbles, options.cancelable, options.detail);
+ return evt;
+ };
+ window.CustomEvent.prototype = window.Event.prototype;
+}
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 59e526ed623..e8e502694d6 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -1,4 +1,10 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, padded-blocks, max-len */
+/* global timeago */
+/* global dateFormat */
+
+/*= require timeago */
+/*= require date.format */
+
(function() {
(function(w) {
var base;
@@ -22,51 +28,66 @@
if (setTimeago == null) {
setTimeago = true;
}
- $timeagoEls.each(function() {
- var $el;
- $el = $(this);
- return $el.attr('title', gl.utils.formatDate($el.attr('datetime')));
+
+ $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);
});
- if (setTimeago) {
- $timeagoEls.timeago();
- $timeagoEls.tooltip('destroy');
- // Recreate with custom template
- return $timeagoEls.tooltip({
- template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
- });
- }
};
- w.gl.utils.shortTimeAgo = function($el) {
- var shortLocale, tmpLocale;
- shortLocale = {
- prefixAgo: null,
- prefixFromNow: null,
- suffixAgo: 'ago',
- suffixFromNow: 'from now',
- seconds: '1 min',
- minute: '1 min',
- minutes: '%d mins',
- hour: '1 hr',
- hours: '%d hrs',
- day: '1 day',
- days: '%d days',
- month: '1 month',
- months: '%d months',
- year: '1 year',
- years: '%d years',
- wordSeparator: ' ',
- numbers: []
+ w.gl.utils.getTimeago = function() {
+ var locale = function(number, index) {
+ return [
+ ['less than a minute ago', 'a while'],
+ ['less than a minute ago', 'in %s seconds'],
+ ['about a minute ago', 'in 1 minute'],
+ ['%s minutes ago', 'in %s minutes'],
+ ['about an hour ago', 'in 1 hour'],
+ ['about %s hours ago', 'in %s hours'],
+ ['a day ago', 'in 1 day'],
+ ['%s days ago', 'in %s days'],
+ ['a week ago', 'in 1 week'],
+ ['%s weeks ago', 'in %s weeks'],
+ ['a month ago', 'in 1 month'],
+ ['%s months ago', 'in %s months'],
+ ['a year ago', 'in 1 year'],
+ ['%s years ago', 'in %s years']
+ ][index];
};
- tmpLocale = $.timeago.settings.strings;
- $el.each(function(el) {
- var $el1;
- $el1 = $(this);
- return $el1.attr('title', gl.utils.formatDate($el.attr('datetime')));
- });
- $.timeago.settings.strings = shortLocale;
- $el.timeago();
- $.timeago.settings.strings = tmpLocale;
+
+ timeago.register('gl_en', locale);
+ return timeago();
+ };
+
+ w.gl.utils.timeFor = function(time, suffix, expiredLabel) {
+ var timefor;
+ if (!time) {
+ return '';
+ }
+ suffix || (suffix = 'remaining');
+ expiredLabel || (expiredLabel = 'Past due');
+ timefor = gl.utils.getTimeago().format(time).replace('in', '');
+ if (timefor.indexOf('ago') > -1) {
+ timefor = expiredLabel;
+ } else {
+ timefor = timefor.trim() + ' ' + suffix;
+ }
+ return timefor;
+ };
+
+ w.gl.utils.renderTimeago = function($element) {
+ var timeagoInstance = gl.utils.getTimeago();
+ timeagoInstance.render($element, 'gl_en');
};
w.gl.utils.getDayDifference = function(a, b) {
@@ -75,7 +96,7 @@
var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
return Math.floor((date2 - date1) / millisecondsPerDay);
- }
+ };
})(window);
diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js
index dafc006d2e5..3c9ad0e67c8 100644
--- a/app/assets/javascripts/lib/utils/notify.js
+++ b/app/assets/javascripts/lib/utils/notify.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, max-len */
+
(function() {
(function(w) {
var notificationGranted, notifyMe, notifyPermissions;
diff --git a/app/assets/javascripts/lib/utils/pretty_time.js.es6 b/app/assets/javascripts/lib/utils/pretty_time.js.es6
new file mode 100644
index 00000000000..ccaf447eb0b
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/pretty_time.js.es6
@@ -0,0 +1,67 @@
+(() => {
+ /*
+ * TODO: Make these methods more configurable (e.g. parseSeconds timePeriodContstraints,
+ * stringifyTime condensed or non-condensed, abbreviateTimelengths)
+ * */
+
+ class PrettyTime {
+
+ /*
+ * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # }
+ * Seconds can be negative or positive, zero or non-zero.
+ */
+ static parseSeconds(seconds) {
+ const DAYS_PER_WEEK = 5;
+ const HOURS_PER_DAY = 8;
+ const MINUTES_PER_HOUR = 60;
+ const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR;
+ const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
+
+ const timePeriodConstraints = {
+ weeks: MINUTES_PER_WEEK,
+ days: MINUTES_PER_DAY,
+ hours: MINUTES_PER_HOUR,
+ minutes: 1,
+ };
+
+ let unorderedMinutes = PrettyTime.secondsToMinutes(seconds);
+
+ return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => {
+ const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
+
+ unorderedMinutes -= (periodCount * minutesPerPeriod);
+
+ return periodCount;
+ });
+ }
+
+ /*
+ * Accepts a timeObject and returns a condensed string representation of it
+ * (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
+ */
+
+ static stringifyTime(timeObject) {
+ const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => {
+ const isNonZero = !!unitValue;
+ return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
+ }, '').trim();
+ return reducedTime.length ? reducedTime : '0m';
+ }
+
+ /*
+ * Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns
+ * the first non-zero unit/value pair.
+ */
+
+ static abbreviateTime(timeStr) {
+ return timeStr.split(' ')
+ .filter(unitStr => unitStr.charAt(0) !== '0')[0];
+ }
+
+ static secondsToMinutes(seconds) {
+ return Math.abs(seconds / 60);
+ }
+ }
+
+ gl.PrettyTime = PrettyTime;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 98f9815ff05..ac44b81ee22 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, semi, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, padded-blocks, max-len */
(function() {
(function(w) {
var base;
@@ -112,6 +112,9 @@
gl.text.removeListeners = function(form) {
return $('.js-md', form).off();
};
+ gl.text.humanize = function(string) {
+ return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
+ }
return gl.text.truncate = function(string, maxLength) {
return string.substr(0, (maxLength - 3)) + '...';
};
diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js
index 4fd1e3fc1d3..961859dfb5b 100644
--- a/app/assets/javascripts/lib/utils/type_utility.js
+++ b/app/assets/javascripts/lib/utils/type_utility.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, no-return-assign, padded-blocks, max-len */
(function() {
(function(w) {
var base;
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 44a66a915e3..6872186cd7f 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, no-plusplus, guard-for-in, no-restricted-syntax, prefer-template, quotes, padded-blocks, max-len */
(function() {
(function(w) {
var base;
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index ea5a60bb78e..9af89b79f84 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, spaced-comment, radix, no-else-return, max-len, no-plusplus, padded-blocks */
+
// LineHighlighter
//
// Handles single- and multi-line selection and highlight for blob views.
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index d4f86534f0c..0ae6df311bb 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, padded-blocks */
+/* global Turbolinks */
+
(function() {
Turbolinks.enableProgressBar();
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
index 0bd90c57396..7741cd29793 100644
--- a/app/assets/javascripts/member_expiration_date.js
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, vars-on-top, no-var, object-shorthand, comma-dangle, max-len */
(function() {
// Add datepickers to all `js-access-expiration-date` elements. If those elements are
// children of an element with the `clearable-input` class, and have a sibling
diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js.es6
index 895bc10784f..e3f367a11eb 100644
--- a/app/assets/javascripts/members.js.es6
+++ b/app/assets/javascripts/members.js.es6
@@ -1,38 +1,81 @@
-/* eslint-disable */
-((w) => {
- w.gl = w.gl || {};
+/* eslint-disable class-methods-use-this */
+(() => {
+ window.gl = window.gl || {};
class Members {
constructor() {
this.addListeners();
+ this.initGLDropdown();
}
addListeners() {
$('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
- $('.js-member-update-control').off('change').on('change', this.formSubmit);
- $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess);
+ $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
+ $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
}
+ initGLDropdown() {
+ $('.js-member-permissions-dropdown').each((i, btn) => {
+ const $btn = $(btn);
+
+ $btn.glDropdown({
+ selectable: true,
+ isSelectable(selected, $el) {
+ return !$el.hasClass('is-active');
+ },
+ fieldName: $btn.data('field-name'),
+ id(selected, $el) {
+ return $el.data('id');
+ },
+ toggleLabel(selected, $el) {
+ return $el.text();
+ },
+ clicked: (selected, $link) => {
+ this.formSubmit(null, $link);
+ },
+ });
+ });
+ }
+
removeRow(e) {
const $target = $(e.target);
if ($target.hasClass('btn-remove')) {
$target.closest('.member')
- .fadeOut(function () {
+ .fadeOut(function fadeOutMemberRow() {
$(this).remove();
});
}
}
- formSubmit() {
- $(this).closest('form').trigger("submit.rails").end().disable();
+ formSubmit(e, $el = null) {
+ const $this = e ? $(e.currentTarget) : $el;
+ const { $toggle, $dateInput } = this.getMemberListItems($this);
+
+ $this.closest('form').trigger('submit.rails');
+
+ $toggle.disable();
+ $dateInput.disable();
}
- formSuccess() {
- $(this).find('.js-member-update-control').enable();
+ formSuccess(e) {
+ const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member'));
+
+ $toggle.enable();
+ $dateInput.enable();
+ }
+
+ getMemberListItems($el) {
+ const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`);
+
+ return {
+ $memberListItem,
+ $toggle: $memberListItem.find('.dropdown-menu-toggle'),
+ $dateInput: $memberListItem.find('.js-access-expiration-date'),
+ };
}
}
gl.Members = Members;
-})(window);
+})();
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
index 6da3942ea52..f95b079c972 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, prefer-const, no-new, padded-blocks, no-param-reassign, semi, max-len */
+/* global Vue */
+/* global ace */
+/* global Flash */
+
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
@@ -36,7 +40,7 @@
this.loadEditor();
}
},
- ready() {
+ mounted() {
if (this.file.loadEditor) {
this.loadEditor();
}
diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
index 23c4618af70..74544b7d0c7 100644
--- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */
+/* global Vue */
+
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6
deleted file mode 100644
index 797850262cc..00000000000
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6
+++ /dev/null
@@ -1,15 +0,0 @@
-/* eslint-disable */
-((global) => {
-
- global.mergeConflicts = global.mergeConflicts || {};
-
- global.mergeConflicts.parallelConflictLine = Vue.extend({
- props: {
- file: Object,
- line: Object
- },
- mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
- template: '#parallel-conflict-line'
- });
-
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
index 1b3e9901f1e..78c00c31c16 100644
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */
+/* global Vue */
+
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
@@ -7,10 +9,22 @@
props: {
file: Object
},
- mixins: [global.mergeConflicts.utils],
- components: {
- 'parallel-conflict-line': gl.mergeConflicts.parallelConflictLine
- }
+ mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
+ template: `
+ <table>
+ <tr class="line_holder parallel" v-for="section in file.parallelLines">
+ <template v-for="line in section">
+ <td class="diff-line-num header" :class="lineCssClass(line)" v-if="line.isHeader"></td>
+ <td class="line_content header" :class="lineCssClass(line)" v-if="line.isHeader">
+ <strong>{{line.richText}}</strong>
+ <button class="btn" @click="handleSelected(file, line.id, line.section)">{{line.buttonTitle}}</button>
+ </td>
+ <td class="diff-line-num old_line" :class="lineCssClass(line)" v-if="!line.isHeader">{{line.lineNumber}}</td>
+ <td class="line_content parallel" :class="lineCssClass(line)" v-if="!line.isHeader" v-html="line.richText"></td>
+ </template>
+ </tr>
+ </table>
+ `,
});
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
index 8a7519b0786..8df3170edac 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-param-reassign, comma-dangle, no-extra-semi, padded-blocks */
+
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
index f94e51e783c..53b44007510 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, object-shorthand, no-dupe-keys, no-param-reassign, no-plusplus, camelcase, prefer-const, no-nested-ternary, no-continue, semi, func-call-spacing, no-spaced-func, padded-blocks, max-len */
+/* global Cookies */
+/* global Vue */
+
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
index 222a5dcfc2e..83520702f9b 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable new-cap, comma-dangle, no-new, semi */
+/* global Vue */
+/* global Flash */
+
//= require vue
//= require ./merge_conflict_store
//= require ./merge_conflict_service
@@ -6,7 +9,6 @@
//= require ./mixins/line_conflict_actions
//= require ./components/diff_file_editor
//= require ./components/inline_conflict_lines
-//= require ./components/parallel_conflict_line
//= require ./components/parallel_conflict_lines
$(() => {
@@ -49,7 +51,7 @@ $(() => {
mergeConflictsStore.setLoadingState(false);
this.$nextTick(() => {
- $(conflictsEl.querySelectorAll('.js-syntax-highlight')).syntaxHighlight();
+ $('.js-syntax-highlight').syntaxHighlight();
});
});
},
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
index c8de586aa21..e89b35d5407 100644
--- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-param-reassign, comma-dangle, padded-blocks */
+
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
index 88c3a20ce13..a4aca85d460 100644
--- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-param-reassign, quote-props, comma-dangle, padded-blocks */
+
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index d3bd1e846c1..244c2f6746c 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len, prefer-arrow-callback */
+/* global MergeRequestTabs */
/*= require jquery.waitforimages */
/*= require task_list */
@@ -26,6 +27,7 @@
// Prevent duplicate event bindings
this.disableTaskList();
this.initMRBtnListeners();
+ this.initCommitMessageListeners();
if ($("a.btn-close").length) {
this.initTaskList();
}
@@ -40,7 +42,7 @@
if (window.mrTabs) {
window.mrTabs.unbindEvents();
}
- window.mrTabs = new MergeRequestTabs(this.opts);
+ window.mrTabs = new gl.MergeRequestTabs(this.opts);
};
MergeRequest.prototype.showAllCommits = function() {
@@ -107,6 +109,26 @@
// note so that we can re-use its form here
};
+ MergeRequest.prototype.initCommitMessageListeners = function() {
+ var textarea = $('textarea.js-commit-message');
+
+ $('a.js-with-description-link').on('click', function(e) {
+ e.preventDefault();
+
+ textarea.val(textarea.data('messageWithDescription'));
+ $('p.js-with-description-hint').hide();
+ $('p.js-without-description-hint').show();
+ });
+
+ $('a.js-without-description-link').on('click', function(e) {
+ e.preventDefault();
+
+ textarea.val(textarea.data('messageWithoutDescription'));
+ $('p.js-with-description-hint').show();
+ $('p.js-without-description-hint').hide();
+ });
+ };
+
return MergeRequest;
})();
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
deleted file mode 100644
index 860ee5df57e..00000000000
--- a/app/assets/javascripts/merge_request_tabs.js
+++ /dev/null
@@ -1,441 +0,0 @@
-/* eslint-disable */
-// MergeRequestTabs
-//
-// Handles persisting and restoring the current tab selection and lazily-loading
-// content on the MergeRequests#show page.
-//
-/*= require js.cookie */
-
-//
-// ### Example Markup
-//
-// <ul class="nav-links merge-request-tabs">
-// <li class="notes-tab active">
-// <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1">
-// Discussion
-// </a>
-// </li>
-// <li class="commits-tab">
-// <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits">
-// Commits
-// </a>
-// </li>
-// <li class="diffs-tab">
-// <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs">
-// Diffs
-// </a>
-// </li>
-// </ul>
-//
-// <div class="tab-content">
-// <div class="notes tab-pane active" id="notes">
-// Notes Content
-// </div>
-// <div class="commits tab-pane" id="commits">
-// Commits Content
-// </div>
-// <div class="diffs tab-pane" id="diffs">
-// Diffs Content
-// </div>
-// </div>
-//
-// <div class="mr-loading-status">
-// <div class="loading">
-// Loading Animation
-// </div>
-// </div>
-//
-(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
- this.MergeRequestTabs = (function() {
- MergeRequestTabs.prototype.diffsLoaded = false;
-
- MergeRequestTabs.prototype.buildsLoaded = false;
-
- MergeRequestTabs.prototype.pipelinesLoaded = false;
-
- MergeRequestTabs.prototype.commitsLoaded = false;
-
- MergeRequestTabs.prototype.fixedLayoutPref = null;
-
- function MergeRequestTabs(opts) {
- this.opts = opts != null ? opts : {};
- this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
-
- this.buildsLoaded = this.opts.buildsLoaded || false;
-
- this.setCurrentAction = bind(this.setCurrentAction, this);
- this.tabShown = bind(this.tabShown, this);
- this.showTab = bind(this.showTab, this);
- // Store the `location` object, allowing for easier stubbing in tests
- this._location = location;
- this.bindEvents();
- this.activateTab(this.opts.action);
- this.initAffix();
- }
-
- MergeRequestTabs.prototype.bindEvents = function() {
- $(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
- $(document).on('click', '.js-show-tab', this.showTab);
- };
-
- MergeRequestTabs.prototype.unbindEvents = function() {
- $(document).off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
- $(document).off('click', '.js-show-tab', this.showTab);
- };
-
- MergeRequestTabs.prototype.showTab = function(event) {
- event.preventDefault();
- return this.activateTab($(event.target).data('action'));
- };
-
- MergeRequestTabs.prototype.tabShown = function(event) {
- var $target, action, navBarHeight;
- $target = $(event.target);
- action = $target.data('action');
- if (action === 'commits') {
- this.loadCommits($target.attr('href'));
- this.expandView();
- this.resetViewContainer();
- } else if (this.isDiffAction(action)) {
- this.loadDiff($target.attr('href'));
- if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') {
- this.shrinkView();
- }
- if (this.diffViewType() === 'parallel') {
- this.expandViewContainer();
- }
- navBarHeight = $('.navbar-gitlab').outerHeight();
- $.scrollTo(".merge-request-details .merge-request-tabs", {
- offset: -navBarHeight
- });
- } else if (action === 'builds') {
- this.loadBuilds($target.attr('href'));
- this.expandView();
- this.resetViewContainer();
- } else if (action === 'pipelines') {
- this.loadPipelines($target.attr('href'));
- this.expandView();
- this.resetViewContainer();
- } else {
- this.expandView();
- this.resetViewContainer();
- }
- if (this.opts.setUrl) {
- this.setCurrentAction(action);
- }
- };
-
- MergeRequestTabs.prototype.scrollToElement = function(container) {
- var $el, navBarHeight;
- if (window.location.hash) {
- navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
- $el = $(container + " " + window.location.hash + ":not(.match)");
- if ($el.length) {
- return $.scrollTo(container + " " + window.location.hash + ":not(.match)", {
- offset: -navBarHeight
- });
- }
- }
- };
-
- // Activate a tab based on the current action
- MergeRequestTabs.prototype.activateTab = function(action) {
- if (action === 'show') {
- action = 'notes';
- }
- $(".merge-request-tabs a[data-action='" + action + "']").tab('show').trigger('shown.bs.tab');
- };
-
- // Replaces the current Merge Request-specific action in the URL with a new one
- //
- // If the action is "notes", the URL is reset to the standard
- // `MergeRequests#show` route.
- //
- // Examples:
- //
- // location.pathname # => "/namespace/project/merge_requests/1"
- // setCurrentAction('diffs')
- // location.pathname # => "/namespace/project/merge_requests/1/diffs"
- //
- // location.pathname # => "/namespace/project/merge_requests/1/diffs"
- // setCurrentAction('notes')
- // location.pathname # => "/namespace/project/merge_requests/1"
- //
- // location.pathname # => "/namespace/project/merge_requests/1/diffs"
- // setCurrentAction('commits')
- // location.pathname # => "/namespace/project/merge_requests/1/commits"
- //
- // Returns the new URL String
- MergeRequestTabs.prototype.setCurrentAction = function(action) {
- var new_state;
- // Normalize action, just to be safe
- if (action === 'show') {
- action = 'notes';
- }
- this.currentAction = action;
- // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs'
- new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
-
- // Append the new action if we're on a tab other than 'notes'
- if (action !== 'notes') {
- new_state += "/" + action;
- }
- // Ensure parameters and hash come along for the ride
- new_state += this._location.search + this._location.hash;
- history.replaceState({
- turbolinks: true,
- url: new_state
- // Replace the current history state with the new one without breaking
- // Turbolinks' history.
- //
- // See https://github.com/rails/turbolinks/issues/363
- }, document.title, new_state);
- return new_state;
- };
-
- MergeRequestTabs.prototype.loadCommits = function(source) {
- if (this.commitsLoaded) {
- return;
- }
- return this._get({
- url: source + ".json",
- success: (function(_this) {
- return function(data) {
- document.querySelector("div#commits").innerHTML = data.html;
- gl.utils.localTimeAgo($('.js-timeago', 'div#commits'));
- _this.commitsLoaded = true;
- return _this.scrollToElement("#commits");
- };
- })(this)
- });
- };
-
- MergeRequestTabs.prototype.loadDiff = function(source) {
- if (this.diffsLoaded) {
- return;
- }
-
- // We extract pathname for the current Changes tab anchor href
- // some pages like MergeRequestsController#new has query parameters on that anchor
- var url = gl.utils.parseUrl(source);
-
- return this._get({
- url: (url.pathname + ".json") + this._location.search,
- success: (function(_this) {
- return function(data) {
- $('#diffs').html(data.html);
-
- if (typeof DiffNotesApp !== 'undefined') {
- DiffNotesApp.compileComponents();
- }
-
- gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
- $('#diffs .js-syntax-highlight').syntaxHighlight();
- $('#diffs .diff-file').singleFileDiff();
- if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) {
- _this.expandViewContainer();
- }
- _this.diffsLoaded = true;
- var anchoredDiff = gl.utils.getLocationHash();
- if (anchoredDiff) _this.openAnchoredDiff(anchoredDiff, function() {
- _this.scrollToElement("#diffs");
- _this.highlighSelectedLine();
- });
- _this.filesCommentButton = $('.files .diff-file').filesCommentButton();
- return $(document).off('click', '.diff-line-num a').on('click', '.diff-line-num a', function(e) {
- e.preventDefault();
- window.location.hash = $(e.currentTarget).attr('href');
- _this.highlighSelectedLine();
- return _this.scrollToElement("#diffs");
- });
- };
- })(this)
- });
- };
-
- MergeRequestTabs.prototype.openAnchoredDiff = function(anchoredDiff, cb) {
- var diffTitle = $('#file-path-' + anchoredDiff);
- var diffFile = diffTitle.closest('.diff-file');
- var nothingHereBlock = $('.nothing-here-block:visible', diffFile);
- if (nothingHereBlock.length) {
- diffFile.singleFileDiff(true, cb);
- } else {
- cb();
- }
- };
-
- MergeRequestTabs.prototype.highlighSelectedLine = function() {
- var $diffLine, diffLineTop, hashClassString, locationHash, navBarHeight;
- $('.hll').removeClass('hll');
- locationHash = window.location.hash;
- if (locationHash !== '') {
- dataLineString = '[data-line-code="' + locationHash.replace('#', '') + '"]';
- $diffLine = $(locationHash + ":not(.match)", $('#diffs'));
- if (!$diffLine.is('tr')) {
- $diffLine = $('#diffs').find("td" + locationHash + ", td" + dataLineString);
- } else {
- $diffLine = $diffLine.find('td');
- }
- if ($diffLine.length) {
- $diffLine.addClass('hll');
- diffLineTop = $diffLine.offset().top;
- return navBarHeight = $('.navbar-gitlab').outerHeight();
- }
- }
- };
-
- MergeRequestTabs.prototype.loadBuilds = function(source) {
- if (this.buildsLoaded) {
- return;
- }
- return this._get({
- url: source + ".json",
- success: (function(_this) {
- return function(data) {
- document.querySelector("div#builds").innerHTML = data.html;
- gl.utils.localTimeAgo($('.js-timeago', 'div#builds'));
- _this.buildsLoaded = true;
- if (!this.pipelines) this.pipelines = new gl.Pipelines();
- return _this.scrollToElement("#builds");
- };
- })(this)
- });
- };
-
- MergeRequestTabs.prototype.loadPipelines = function(source) {
- if (this.pipelinesLoaded) {
- return;
- }
- return this._get({
- url: source + ".json",
- success: function(data) {
- $('#pipelines').html(data.html);
- gl.utils.localTimeAgo($('.js-timeago', '#pipelines'));
- this.pipelinesLoaded = true;
- return this.scrollToElement("#pipelines");
- }.bind(this)
- });
- };
-
- // Show or hide the loading spinner
- //
- // status - Boolean, true to show, false to hide
- MergeRequestTabs.prototype.toggleLoading = function(status) {
- return $('.mr-loading-status .loading').toggle(status);
- };
-
- MergeRequestTabs.prototype._get = function(options) {
- var defaults;
- defaults = {
- beforeSend: (function(_this) {
- return function() {
- return _this.toggleLoading(true);
- };
- })(this),
- complete: (function(_this) {
- return function() {
- return _this.toggleLoading(false);
- };
- })(this),
- dataType: 'json',
- type: 'GET'
- };
- options = $.extend({}, defaults, options);
- return $.ajax(options);
- };
-
- MergeRequestTabs.prototype.diffViewType = function() {
- return $('.inline-parallel-buttons a.active').data('view-type');
- };
-
- MergeRequestTabs.prototype.isDiffAction = function(action) {
- return action === 'diffs' || action === 'new/diffs'
- };
-
- MergeRequestTabs.prototype.expandViewContainer = function() {
- var $wrapper = $('.content-wrapper .container-fluid');
- if (this.fixedLayoutPref === null) {
- this.fixedLayoutPref = $wrapper.hasClass('container-limited');
- }
- $wrapper.removeClass('container-limited');
- };
-
- MergeRequestTabs.prototype.resetViewContainer = function() {
- if (this.fixedLayoutPref !== null) {
- $('.content-wrapper .container-fluid')
- .toggleClass('container-limited', this.fixedLayoutPref);
- }
- };
-
- MergeRequestTabs.prototype.shrinkView = function() {
- var $gutterIcon;
- $gutterIcon = $('.js-sidebar-toggle i:visible');
- return setTimeout(function() {
- if ($gutterIcon.is('.fa-angle-double-right')) {
- return $gutterIcon.closest('a').trigger('click', [true]);
- }
- // Wait until listeners are set
- // Only when sidebar is expanded
- }, 0);
- };
-
- MergeRequestTabs.prototype.expandView = function() {
- var $gutterIcon;
- if (Cookies.get('collapsed_gutter') === 'true') {
- return;
- }
- $gutterIcon = $('.js-sidebar-toggle i:visible');
- return setTimeout(function() {
- if ($gutterIcon.is('.fa-angle-double-left')) {
- return $gutterIcon.closest('a').trigger('click', [true]);
- }
- }, 0);
- // Expand the issuable sidebar unless the user explicitly collapsed it
- // Wait until listeners are set
- // Only when sidebar is collapsed
- };
-
- MergeRequestTabs.prototype.initAffix = function () {
- var $tabs = $('.js-tabs-affix');
-
- // Screen space on small screens is usually very sparse
- // So we dont affix the tabs on these
- if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return;
-
- var $diffTabs = $('#diff-notes-app'),
- $fixedNav = $('.navbar-fixed-top'),
- $layoutNav = $('.layout-nav');
-
- $tabs.off('affix.bs.affix affix-top.bs.affix')
- .affix({
- offset: {
- top: function () {
- var tabsTop = $diffTabs.offset().top - $tabs.height();
- tabsTop = tabsTop - ($fixedNav.height() + $layoutNav.height());
-
- return tabsTop;
- }
- }
- }).on('affix.bs.affix', function () {
- $diffTabs.css({
- marginTop: $tabs.height()
- });
- }).on('affix-top.bs.affix', function () {
- $diffTabs.css({
- marginTop: ''
- });
- });
-
- // Fix bug when reloading the page already scrolling
- if ($tabs.hasClass('affix')) {
- $tabs.trigger('affix.bs.affix');
- }
- };
-
- return MergeRequestTabs;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6
new file mode 100644
index 00000000000..3ec0f1fd613
--- /dev/null
+++ b/app/assets/javascripts/merge_request_tabs.js.es6
@@ -0,0 +1,389 @@
+/* eslint-disable no-new, class-methods-use-this */
+/* global Breakpoints */
+/* global Cookies */
+/* global DiffNotesApp */
+/* global Flash */
+
+/*= require js.cookie */
+/*= require breakpoints */
+
+/* eslint-disable max-len */
+// MergeRequestTabs
+//
+// Handles persisting and restoring the current tab selection and lazily-loading
+// content on the MergeRequests#show page.
+//
+// ### Example Markup
+//
+// <ul class="nav-links merge-request-tabs">
+// <li class="notes-tab active">
+// <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1">
+// Discussion
+// </a>
+// </li>
+// <li class="commits-tab">
+// <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits">
+// Commits
+// </a>
+// </li>
+// <li class="diffs-tab">
+// <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs">
+// Diffs
+// </a>
+// </li>
+// </ul>
+//
+// <div class="tab-content">
+// <div class="notes tab-pane active" id="notes">
+// Notes Content
+// </div>
+// <div class="commits tab-pane" id="commits">
+// Commits Content
+// </div>
+// <div class="diffs tab-pane" id="diffs">
+// Diffs Content
+// </div>
+// </div>
+//
+// <div class="mr-loading-status">
+// <div class="loading">
+// Loading Animation
+// </div>
+// </div>
+//
+/* eslint-enable max-len */
+
+(() => {
+ // Store the `location` object, allowing for easier stubbing in tests
+ let location = window.location;
+
+ class MergeRequestTabs {
+
+ constructor({ action, setUrl, buildsLoaded, stubLocation } = {}) {
+ this.diffsLoaded = false;
+ this.buildsLoaded = false;
+ this.pipelinesLoaded = false;
+ this.commitsLoaded = false;
+ this.fixedLayoutPref = null;
+
+ this.setUrl = setUrl !== undefined ? setUrl : true;
+ this.buildsLoaded = buildsLoaded || false;
+
+ this.setCurrentAction = this.setCurrentAction.bind(this);
+ this.tabShown = this.tabShown.bind(this);
+ this.showTab = this.showTab.bind(this);
+
+ if (stubLocation) {
+ location = stubLocation;
+ }
+
+ this.bindEvents();
+ this.activateTab(action);
+ this.initAffix();
+ }
+
+ bindEvents() {
+ $(document)
+ .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
+ .on('click', '.js-show-tab', this.showTab);
+ }
+
+ unbindEvents() {
+ $(document)
+ .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
+ .off('click', '.js-show-tab', this.showTab);
+ }
+
+ showTab(e) {
+ e.preventDefault();
+ this.activateTab($(e.target).data('action'));
+ }
+
+ tabShown(e) {
+ const $target = $(e.target);
+ const action = $target.data('action');
+
+ if (action === 'commits') {
+ this.loadCommits($target.attr('href'));
+ this.expandView();
+ this.resetViewContainer();
+ } else if (this.isDiffAction(action)) {
+ this.loadDiff($target.attr('href'));
+ if (Breakpoints.get().getBreakpointSize() !== 'lg') {
+ this.shrinkView();
+ }
+ if (this.diffViewType() === 'parallel') {
+ this.expandViewContainer();
+ }
+ const navBarHeight = $('.navbar-gitlab').outerHeight();
+ $.scrollTo('.merge-request-details .merge-request-tabs', {
+ offset: -navBarHeight,
+ });
+ } else if (action === 'builds') {
+ this.loadBuilds($target.attr('href'));
+ this.expandView();
+ this.resetViewContainer();
+ } else if (action === 'pipelines') {
+ this.loadPipelines($target.attr('href'));
+ this.expandView();
+ this.resetViewContainer();
+ } else {
+ this.expandView();
+ this.resetViewContainer();
+ }
+ if (this.setUrl) {
+ this.setCurrentAction(action);
+ }
+ }
+
+ scrollToElement(container) {
+ if (location.hash) {
+ const offset = 0 - (
+ $('.navbar-gitlab').outerHeight() +
+ $('.layout-nav').outerHeight() +
+ $('.js-tabs-affix').outerHeight()
+ );
+ const $el = $(`${container} ${location.hash}:not(.match)`);
+ if ($el.length) {
+ $.scrollTo($el[0], { offset });
+ }
+ }
+ }
+
+ // Activate a tab based on the current action
+ activateTab(action) {
+ const activate = action === 'show' ? 'notes' : action;
+ // important note: the .tab('show') method triggers 'shown.bs.tab' event itself
+ $(`.merge-request-tabs a[data-action='${activate}']`).tab('show');
+ }
+
+ // Replaces the current Merge Request-specific action in the URL with a new one
+ //
+ // If the action is "notes", the URL is reset to the standard
+ // `MergeRequests#show` route.
+ //
+ // Examples:
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1"
+ // setCurrentAction('diffs')
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ // setCurrentAction('notes')
+ // location.pathname # => "/namespace/project/merge_requests/1"
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ // setCurrentAction('commits')
+ // location.pathname # => "/namespace/project/merge_requests/1/commits"
+ //
+ // Returns the new URL String
+ setCurrentAction(action) {
+ this.currentAction = action === 'show' ? 'notes' : action;
+
+ // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs'
+ let newState = location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
+
+ // Append the new action if we're on a tab other than 'notes'
+ if (this.currentAction !== 'notes') {
+ newState += `/${this.currentAction}`;
+ }
+
+ // Ensure parameters and hash come along for the ride
+ newState += location.search + location.hash;
+
+ // 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);
+
+ return newState;
+ }
+
+ loadCommits(source) {
+ if (this.commitsLoaded) {
+ return;
+ }
+ this.ajaxGet({
+ url: `${source}.json`,
+ success: (data) => {
+ document.querySelector('div#commits').innerHTML = data.html;
+ gl.utils.localTimeAgo($('.js-timeago', 'div#commits'));
+ this.commitsLoaded = true;
+ this.scrollToElement('#commits');
+ },
+ });
+ }
+
+ loadDiff(source) {
+ if (this.diffsLoaded) {
+ return;
+ }
+
+ // We extract pathname for the current Changes tab anchor href
+ // some pages like MergeRequestsController#new has query parameters on that anchor
+ const urlPathname = gl.utils.parseUrlPathname(source);
+
+ this.ajaxGet({
+ url: `${urlPathname}.json${location.search}`,
+ success: (data) => {
+ $('#diffs').html(data.html);
+
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
+ }
+
+ gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
+ $('#diffs .js-syntax-highlight').syntaxHighlight();
+
+ if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) {
+ this.expandViewContainer();
+ }
+ this.diffsLoaded = true;
+
+ const diffPage = new gl.Diff();
+
+ const locationHash = gl.utils.getLocationHash();
+ const anchoredDiff = locationHash && locationHash.split('_')[0];
+ if (anchoredDiff) {
+ diffPage.openAnchoredDiff(anchoredDiff, () => this.scrollToElement('#diffs'));
+ }
+ },
+ });
+ }
+
+ loadBuilds(source) {
+ if (this.buildsLoaded) {
+ return;
+ }
+ this.ajaxGet({
+ url: `${source}.json`,
+ success: (data) => {
+ document.querySelector('div#builds').innerHTML = data.html;
+ gl.utils.localTimeAgo($('.js-timeago', 'div#builds'));
+ this.buildsLoaded = true;
+ new gl.Pipelines();
+ this.scrollToElement('#builds');
+ },
+ });
+ }
+
+ 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');
+ },
+ });
+ }
+
+ // Show or hide the loading spinner
+ //
+ // status - Boolean, true to show, false to hide
+ toggleLoading(status) {
+ $('.mr-loading-status .loading').toggle(status);
+ }
+
+ ajaxGet(options) {
+ const defaults = {
+ beforeSend: () => this.toggleLoading(true),
+ error: () => new Flash('An error occurred while fetching this tab.', 'alert'),
+ complete: () => this.toggleLoading(false),
+ dataType: 'json',
+ type: 'GET',
+ };
+ $.ajax($.extend({}, defaults, options));
+ }
+
+ diffViewType() {
+ return $('.inline-parallel-buttons a.active').data('view-type');
+ }
+
+ isDiffAction(action) {
+ return action === 'diffs' || action === 'new/diffs';
+ }
+
+ expandViewContainer() {
+ const $wrapper = $('.content-wrapper .container-fluid');
+ if (this.fixedLayoutPref === null) {
+ this.fixedLayoutPref = $wrapper.hasClass('container-limited');
+ }
+ $wrapper.removeClass('container-limited');
+ }
+
+ resetViewContainer() {
+ if (this.fixedLayoutPref !== null) {
+ $('.content-wrapper .container-fluid')
+ .toggleClass('container-limited', this.fixedLayoutPref);
+ }
+ }
+
+ shrinkView() {
+ const $gutterIcon = $('.js-sidebar-toggle i:visible');
+
+ // Wait until listeners are set
+ setTimeout(() => {
+ // Only when sidebar is expanded
+ if ($gutterIcon.is('.fa-angle-double-right')) {
+ $gutterIcon.closest('a').trigger('click', [true]);
+ }
+ }, 0);
+ }
+
+ // Expand the issuable sidebar unless the user explicitly collapsed it
+ expandView() {
+ if (Cookies.get('collapsed_gutter') === 'true') {
+ return;
+ }
+ const $gutterIcon = $('.js-sidebar-toggle i:visible');
+
+ // Wait until listeners are set
+ setTimeout(() => {
+ // Only when sidebar is collapsed
+ if ($gutterIcon.is('.fa-angle-double-left')) {
+ $gutterIcon.closest('a').trigger('click', [true]);
+ }
+ }, 0);
+ }
+
+ initAffix() {
+ const $tabs = $('.js-tabs-affix');
+
+ // Screen space on small screens is usually very sparse
+ // So we dont affix the tabs on these
+ 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()
+ ),
+ },
+ })
+ .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() }))
+ .on('affix-top.bs.affix', () => $diffTabs.css({ marginTop: '' }));
+
+ // Fix bug when reloading the page already scrolling
+ if ($tabs.hasClass('affix')) {
+ $tabs.trigger('affix.bs.affix');
+ }
+ }
+ }
+
+ window.gl = window.gl || {};
+ window.gl.MergeRequestTabs = MergeRequestTabs;
+})();
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
index 3a2fe454b68..e47047c4cca 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -1,5 +1,10 @@
-/* eslint-disable */
- ((global) => {
+/* eslint-disable max-len, no-var, func-names, space-before-function-paren, vars-on-top, no-plusplus, comma-dangle, no-return-assign, consistent-return, no-param-reassign, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, prefer-arrow-callback, no-unused-vars, no-underscore-dangle, no-shadow, no-mixed-operators, template-curly-spacing, camelcase, default-case, wrap-iife, semi, padded-blocks */
+/* global notify */
+/* global notifyPermissions */
+/* global merge_request_widget */
+/* global Turbolinks */
+
+((global) => {
var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>">
@@ -27,7 +32,7 @@
</div>
</div>`;
- global.MergeRequestWidget = (function() {
+ global.MergeRequestWidget = (function() {
function MergeRequestWidget(opts) {
// Initialize MergeRequestWidget behavior
//
@@ -40,19 +45,26 @@
$('#modal_merge_info').modal({
show: false
});
- this.firstCICheck = true;
- this.readyForCICheck = false;
- this.readyForCIEnvironmentCheck = false;
- this.cancel = false;
- clearInterval(this.fetchBuildStatusInterval);
- clearInterval(this.fetchBuildEnvironmentStatusInterval);
this.clearEventListeners();
this.addEventListeners();
this.getCIStatus(false);
- this.getCIEnvironmentsStatus();
this.retrieveSuccessIcon();
- this.pollCIStatus();
- this.pollCIEnvironmentsStatus();
+
+ this.ciStatusInterval = new global.SmartInterval({
+ callback: this.getCIStatus.bind(this, true),
+ startingInterval: 10000,
+ maxInterval: 30000,
+ hiddenInterval: 120000,
+ incrementByFactorOf: 5000,
+ });
+ this.ciEnvironmentStatusInterval = new global.SmartInterval({
+ callback: this.getCIEnvironmentsStatus.bind(this),
+ startingInterval: 30000,
+ maxInterval: 120000,
+ hiddenInterval: 240000,
+ incrementByFactorOf: 15000,
+ immediateExecution: true,
+ });
notifyPermissions();
}
@@ -60,21 +72,14 @@
return $(document).off('page:change.merge_request');
};
- MergeRequestWidget.prototype.cancelPolling = function() {
- return this.cancel = true;
- };
-
MergeRequestWidget.prototype.addEventListeners = function() {
var allowedPages;
allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes'];
- return $(document).on('page:change.merge_request', (function(_this) {
+ $(document).on('page:change.merge_request', (function(_this) {
return function() {
var page;
page = $('body').data('page').split(':').last();
if (allowedPages.indexOf(page) < 0) {
- clearInterval(_this.fetchBuildStatusInterval);
- clearInterval(_this.fetchBuildEnvironmentStatusInterval);
- _this.cancelPolling();
return _this.clearEventListeners();
}
};
@@ -82,10 +87,10 @@
};
MergeRequestWidget.prototype.retrieveSuccessIcon = function() {
- const $ciSuccessIcon = $('.js-success-icon');
- this.$ciSuccessIcon = $ciSuccessIcon.html();
- $ciSuccessIcon.remove();
- }
+ const $ciSuccessIcon = $('.js-success-icon');
+ this.$ciSuccessIcon = $ciSuccessIcon.html();
+ $ciSuccessIcon.remove();
+ }
MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) {
if (deleteSourceBranch == null) {
@@ -101,7 +106,7 @@
urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
return window.location.href = window.location.pathname + urlSuffix;
} else if (data.merge_error) {
- return this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
+ return _this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
} else {
callback = function() {
return merge_request_widget.mergeInProgress(deleteSourceBranch);
@@ -114,6 +119,11 @@
});
};
+ MergeRequestWidget.prototype.cancelPolling = function () {
+ this.ciStatusInterval.cancel();
+ this.ciEnvironmentStatusInterval.cancel();
+ };
+
MergeRequestWidget.prototype.getMergeStatus = function() {
return $.get(this.opts.merge_check_url, function(data) {
return $('.mr-state-widget').replaceWith(data);
@@ -131,18 +141,6 @@
}
};
- MergeRequestWidget.prototype.pollCIStatus = function() {
- return this.fetchBuildStatusInterval = setInterval(((function(_this) {
- return function() {
- if (!_this.readyForCICheck) {
- return;
- }
- _this.getCIStatus(true);
- return _this.readyForCICheck = false;
- };
- })(this)), 10000);
- };
-
MergeRequestWidget.prototype.getCIStatus = function(showNotification) {
var _this;
_this = this;
@@ -150,23 +148,17 @@
return $.getJSON(this.opts.ci_status_url, (function(_this) {
return function(data) {
var message, status, title;
- if (_this.cancel) {
- return;
- }
- _this.readyForCICheck = true;
if (data.status === '') {
return;
}
if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
- if (_this.firstCICheck || data.status !== _this.opts.ci_status && (data.status != null)) {
+ if (data.status !== _this.opts.ci_status && (data.status != null)) {
_this.opts.ci_status = data.status;
_this.showCIStatus(data.status);
if (data.coverage) {
_this.showCICoverage(data.coverage);
}
- // The first check should only update the UI, a notification
- // should only be displayed on status changes
- if (showNotification && !_this.firstCICheck) {
+ if (showNotification) {
status = _this.ciLabelForStatus(data.status);
if (status === "preparing") {
title = _this.opts.ci_title.preparing;
@@ -184,24 +176,13 @@
return Turbolinks.visit(_this.opts.builds_path);
});
}
- return _this.firstCICheck = false;
}
};
})(this));
};
- MergeRequestWidget.prototype.pollCIEnvironmentsStatus = function() {
- this.fetchBuildEnvironmentStatusInterval = setInterval(() => {
- if (!this.readyForCIEnvironmentCheck) return;
- this.getCIEnvironmentsStatus();
- this.readyForCIEnvironmentCheck = false;
- }, 300000);
- };
-
MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() {
$.getJSON(this.opts.ci_environments_status_url, (environments) => {
- if (this.cancel) return;
- this.readyForCIEnvironmentCheck = true;
if (environments && environments.length) this.renderEnvironments(environments);
});
};
@@ -212,13 +193,13 @@
if ($(`.mr-state-widget #${ environment.id }`).length) return;
const $template = $(DEPLOYMENT_TEMPLATE);
if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove();
-
+
if (!environment.stop_url) {
$('.js-stop-env-link', $template).remove();
}
-
+
if (environment.deployed_at && environment.deployed_at_formatted) {
- environment.deployed_at = $.timeago(environment.deployed_at) + '.';
+ environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.';
} else {
$('.js-environment-timeago', $template).remove();
environment.name += '.';
@@ -228,7 +209,7 @@
const template = _.template(templateString)(environment)
this.$widgetBody.before(template);
}
- };
+ };
MergeRequestWidget.prototype.showCIStatus = function(state) {
var allowed_states;
@@ -245,7 +226,7 @@
case "not_found":
return this.setMergeButtonClass('btn-danger');
case "running":
- return this.setMergeButtonClass('btn-warning');
+ return this.setMergeButtonClass('btn-info');
case "success":
case "success_with_warnings":
return this.setMergeButtonClass('btn-create');
@@ -263,11 +244,11 @@
};
MergeRequestWidget.prototype.setMergeButtonClass = function(css_class) {
- return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-warning btn-create').addClass(css_class);
+ return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-info btn-create').addClass(css_class);
};
return MergeRequestWidget;
})();
- })(window.gl || (window.gl = {}));
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js
index 7ad86d8c084..9f8af46c715 100644
--- a/app/assets/javascripts/merged_buttons.js
+++ b/app/assets/javascripts/merged_buttons.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, padded-blocks, max-len */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 9299c96e8ea..42152362e60 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, padded-blocks, max-len */
+/* global Flash */
+
(function() {
this.Milestone = (function() {
Milestone.updateIssue = function(li, issue_url, data) {
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index c909b53dc21..28054b78249 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow, padded-blocks */
+/* global Vue */
+/* global Issuable */
+/* global ListMilestone */
+
(function() {
this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject) {
@@ -162,7 +166,7 @@
if (data.milestone != null) {
data.milestone.namespace = _this.currentProject.namespace;
data.milestone.path = _this.currentProject.path;
- data.milestone.remaining = $.timefor(data.milestone.due_date);
+ data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date);
$value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
} else {
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index d1168227b77..6633f2c2709 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -1,8 +1,10 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, padded-blocks, no-param-reassign, no-cond-assign, max-len */
+/* global Api */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
- this.NamespaceSelect = (function() {
+ window.NamespaceSelect = (function() {
function NamespaceSelect(opts) {
this.onSelectItem = bind(this.onSelectItem, this);
var fieldName, showAny;
@@ -64,7 +66,7 @@
})();
- this.NamespaceSelects = (function() {
+ window.NamespaceSelects = (function() {
function NamespaceSelects(opts) {
var ref;
if (opts == null) {
@@ -74,7 +76,7 @@
this.$dropdowns.each(function(i, dropdown) {
var $dropdown;
$dropdown = $(dropdown);
- return new NamespaceSelect({
+ return new window.NamespaceSelect({
dropdown: $dropdown
});
});
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index 74dbeb94741..64b19a54893 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-plusplus, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len, padded-blocks */
+/* global Raphael */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js
index 8898e7ace43..2367d2497b2 100644
--- a/app/assets/javascripts/network/network.js
+++ b/app/assets/javascripts/network/network.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, padded-blocks, max-len */
+/* global BranchGraph */
+
(function() {
this.Network = (function() {
function Network(opts) {
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
index a192273a180..17833d3419a 100644
--- a/app/assets/javascripts/network/network_bundle.js
+++ b/app/assets/javascripts/network/network_bundle.js
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, padded-blocks, max-len */
+/* 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
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index 0e643b0ff14..29a323dd4c6 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, one-var, space-before-blocks, prefer-rest-params, max-len, vars-on-top, no-plusplus, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, padded-blocks, max-len */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index acb529023fa..8fb8f3e4a5f 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-return-assign, padded-blocks, max-len */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 4976eef2896..a8b9a352870 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, semi, indent, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, radix, padded-blocks */
+/* global Flash */
+/* global GLForm */
+/* global Autosave */
+/* global ResolveService */
/*= require autosave */
/*= require autosize */
@@ -12,7 +16,7 @@
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.Notes = (function() {
- var isMetaKey;
+ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
Notes.interval = null;
@@ -33,6 +37,7 @@
this.resetMainTargetForm = bind(this.resetMainTargetForm, this);
this.refresh = bind(this.refresh, this);
this.keydownNoteText = bind(this.keydownNoteText, this);
+ this.toggleCommitList = bind(this.toggleCommitList, this);
this.notes_url = notes_url;
this.note_ids = note_ids;
this.last_fetched_at = last_fetched_at;
@@ -46,6 +51,7 @@
this.setPollingInterval();
this.setupMainTargetNoteForm();
this.initTaskList();
+ this.collapseLongCommitList();
}
Notes.prototype.addBinding = function() {
@@ -81,10 +87,13 @@
$(document).on("click", ".js-add-diff-note-button", this.addDiffNote);
// hide diff note form
$(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm);
+ // toggle commit list
+ $(document).on("click", '.system-note-commit-list-toggler', this.toggleCommitList);
// fetch notes when tab becomes visible
$(document).on("visibilitychange", this.visibilityChange);
// when issue status changes, we need to refresh data
$(document).on("issuable:change", this.refresh);
+
// when a key is clicked on the notes
return $(document).on("keydown", ".js-note-text", this.keydownNoteText);
};
@@ -108,15 +117,17 @@
$(document).off("click", ".js-note-discard");
$(document).off("keydown", ".js-note-text");
$(document).off('click', '.js-comment-resolve-button');
+ $(document).off("click", '.system-note-commit-list-toggler');
$('.note .js-task-list-container').taskList('disable');
return $(document).off('tasklist:changed', '.note .js-task-list-container');
};
Notes.prototype.keydownNoteText = function(e) {
var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText;
- if (isMetaKey(e)) {
+ if (gl.utils.isMetaKey(e)) {
return;
}
+
$textarea = $(e.target);
// Edit previous note when UP arrow is hit
switch (e.which) {
@@ -156,10 +167,6 @@
}
};
- isMetaKey = function(e) {
- return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
- };
-
Notes.prototype.initRefresh = function() {
clearInterval(Notes.interval);
return Notes.interval = setInterval((function(_this) {
@@ -263,6 +270,7 @@
$notesList.append(note.html).syntaxHighlight();
// Update datetime format on the recent note
gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
+ this.collapseLongCommitList();
this.initTaskList();
this.refresh();
return this.updateNotesCount(1);
@@ -301,7 +309,7 @@
}
row = form.closest("tr");
note_html = $(note.html);
- note_html.syntaxHighlight();
+ note_html.renderGFM();
// is this the first note of discussion?
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
if ((note.original_discussion_id != null) && discussionContainer.length === 0) {
@@ -318,18 +326,18 @@
discussionContainer.append(note_html);
// Init discussion on 'Discussion' page if it is merge request page
if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) {
- $('ul.main-notes-list').append(note.discussion_html).syntaxHighlight();
+ $('ul.main-notes-list').append(note.discussion_html).renderGFM();
}
} else {
// append new note to all matching discussions
discussionContainer.append(note_html);
}
- if (typeof DiffNotesApp !== 'undefined') {
- DiffNotesApp.compileComponents();
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
}
- gl.utils.localTimeAgo($('.js-timeago', note_html), false);
+ gl.utils.localTimeAgo($('.js-timeago'), false);
return this.updateNotesCount(1);
};
@@ -433,9 +441,9 @@
var $form = $(xhr.target);
if ($form.attr('data-resolve-all') != null) {
- var projectPath = $form.data('project-path')
- discussionId = $form.data('discussion-id'),
- mergeRequestId = $form.data('noteable-iid');
+ var projectPath = $form.data('project-path');
+ var discussionId = $form.data('discussion-id');
+ var mergeRequestId = $form.data('noteable-iid');
if (ResolveService != null) {
ResolveService.toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId);
@@ -459,15 +467,15 @@
// Convert returned HTML to a jQuery object so we can modify it further
$html = $(note.html);
gl.utils.localTimeAgo($('.js-timeago', $html));
- $html.syntaxHighlight();
+ $html.renderGFM();
$html.find('.js-task-list-container').taskList('enable');
// Find the note's `li` element by ID and replace it with the updated HTML
$note_li = $('.note-row-' + note.id);
$note_li.replaceWith($html);
- if (typeof DiffNotesApp !== 'undefined') {
- DiffNotesApp.compileComponents();
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
}
};
@@ -559,11 +567,9 @@
note = $(el);
notes = note.closest(".notes");
- if (typeof DiffNotesApp !== "undefined" && DiffNotesApp !== null) {
- ref = DiffNotesApp.$refs[noteId];
-
- if (ref) {
- ref.$destroy(true);
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ if (gl.diffNoteApps[noteId]) {
+ gl.diffNoteApps[noteId].$destroy();
}
}
@@ -643,11 +649,12 @@
form.find('.js-note-target-close').remove();
this.setupNoteForm(form);
- if (typeof DiffNotesApp !== 'undefined') {
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
var $commentBtn = form.find('comment-and-resolve-btn');
$commentBtn
.attr(':discussion-id', "'" + dataHolder.data('discussionId') + "'");
- DiffNotesApp.$compile($commentBtn.get(0));
+
+ gl.diffNotesCompileComponents();
}
form.find(".js-note-text").focus();
@@ -668,7 +675,7 @@
*/
Notes.prototype.addDiffNote = function(e) {
- var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, replyButton, row, rowCssToAdd, targetContent;
+ var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent;
e.preventDefault();
$link = $(e.currentTarget);
row = $link.closest("tr");
@@ -845,9 +852,9 @@
return this.notesCountBadge.text(parseInt(this.notesCountBadge.text()) + updateCount);
};
- Notes.prototype.resolveDiscussion = function () {
- var $this = $(this),
- discussionId = $this.attr('data-discussion-id');
+ Notes.prototype.resolveDiscussion = function() {
+ var $this = $(this);
+ var discussionId = $this.attr('data-discussion-id');
$this
.closest('form')
@@ -856,6 +863,36 @@
.attr('data-project-path', $this.attr('data-project-path'));
};
+ Notes.prototype.toggleCommitList = function(e) {
+ const $element = $(e.target);
+ const $closestSystemCommitList = $element.siblings('.system-note-commit-list');
+
+ $closestSystemCommitList.toggleClass('hide-shade');
+ };
+
+ /**
+ Scans system notes with `ul` elements in system note body
+ then collapse long commit list pushed by user to make it less
+ intrusive.
+ */
+ Notes.prototype.collapseLongCommitList = function() {
+ const systemNotes = $('#notes-list').find('li.system-note').has('ul');
+
+ $.each(systemNotes, function(index, systemNote) {
+ const $systemNote = $(systemNote);
+ const headerMessage = $systemNote.find('.note-text').find('p:first').text().replace(':', '');
+
+ $systemNote.find('.note-header .system-note-message').html(headerMessage);
+
+ if ($systemNote.find('li').length > MAX_VISIBLE_COMMIT_LIST_COUNT) {
+ $systemNote.find('.note-text').addClass('system-note-commit-list');
+ $systemNote.find('.system-note-commit-list-toggler').show();
+ } else {
+ $systemNote.find('.note-text').addClass('system-note-commit-list hide-shade');
+ }
+ });
+ };
+
return Notes;
})();
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index ef3f2c6ae73..324b68a7efc 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, padded-blocks, max-len */
+/* global Flash */
+
(function() {
this.NotificationsDropdown = (function() {
function NotificationsDropdown() {
diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js
index 6fbec8efe9b..2034f9a748a 100644
--- a/app/assets/javascripts/notifications_form.js
+++ b/app/assets/javascripts/notifications_form.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, newline-per-chained-call, comma-dangle, consistent-return, prefer-arrow-callback, padded-blocks, max-len */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js
deleted file mode 100644
index 2e4dc62273e..00000000000
--- a/app/assets/javascripts/pager.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/* eslint-disable */
-(function() {
- this.Pager = {
- init: function(limit, preload, disable, callback) {
- this.limit = limit != null ? limit : 0;
- this.disable = disable != null ? disable : false;
- this.callback = callback != null ? callback : $.noop;
- this.loading = $('.loading').first();
- if (preload) {
- this.offset = 0;
- this.getOld();
- } else {
- this.offset = this.limit;
- }
- return this.initLoadMore();
- },
- getOld: function() {
- this.loading.show();
- return $.ajax({
- type: "GET",
- url: $(".content_list").data('href') || location.href,
- data: "limit=" + this.limit + "&offset=" + this.offset,
- complete: (function(_this) {
- return function() {
- return _this.loading.hide();
- };
- })(this),
- success: function(data) {
- Pager.append(data.count, data.html);
- return Pager.callback();
- },
- dataType: "json"
- });
- },
- append: function(count, html) {
- $(".content_list").append(html);
- if (count > 0) {
- return this.offset += count;
- } else {
- return this.disable = true;
- }
- },
- initLoadMore: function() {
- $(document).unbind('scroll');
- return $(document).endlessScroll({
- bottomPixels: 400,
- fireDelay: 1000,
- fireOnce: true,
- ceaseFire: function() {
- return Pager.disable;
- },
- callback: (function(_this) {
- return function(i) {
- if (!_this.loading.is(':visible')) {
- _this.loading.show();
- return Pager.getOld();
- }
- };
- })(this)
- });
- }
- };
-
-}).call(this);
diff --git a/app/assets/javascripts/pager.js.es6 b/app/assets/javascripts/pager.js.es6
new file mode 100644
index 00000000000..e35cf6d295e
--- /dev/null
+++ b/app/assets/javascripts/pager.js.es6
@@ -0,0 +1,73 @@
+(() => {
+ const ENDLESS_SCROLL_BOTTOM_PX = 400;
+ const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000;
+
+ const Pager = {
+ init(limit = 0, preload = false, disable = false, callback = $.noop) {
+ this.limit = limit;
+ this.offset = this.limit;
+ this.disable = disable;
+ this.callback = callback;
+ this.loading = $('.loading').first();
+ if (preload) {
+ this.offset = 0;
+ this.getOld();
+ }
+ this.initLoadMore();
+ },
+
+ getOld() {
+ this.loading.show();
+ $.ajax({
+ type: 'GET',
+ url: $('.content_list').data('href') || window.location.href,
+ data: `limit=${this.limit}&offset=${this.offset}`,
+ dataType: 'json',
+ error: () => this.loading.hide(),
+ success: (data) => {
+ this.append(data.count, data.html);
+ this.callback();
+
+ // keep loading until we've filled the viewport height
+ if (!this.disable && !this.isScrollable()) {
+ this.getOld();
+ } else {
+ this.loading.hide();
+ }
+ },
+ });
+ },
+
+ append(count, html) {
+ $('.content_list').append(html);
+ if (count > 0) {
+ this.offset += count;
+ } else {
+ this.disable = true;
+ }
+ },
+
+ isScrollable() {
+ const $w = $(window);
+ return $(document).height() > $w.height() + $w.scrollTop() + ENDLESS_SCROLL_BOTTOM_PX;
+ },
+
+ initLoadMore() {
+ $(document).unbind('scroll');
+ $(document).endlessScroll({
+ bottomPixels: ENDLESS_SCROLL_BOTTOM_PX,
+ fireDelay: ENDLESS_SCROLL_FIRE_DELAY_MS,
+ fireOnce: true,
+ ceaseFire: () => this.disable === true,
+ callback: () => {
+ if (!this.loading.is(':visible')) {
+ this.loading.show();
+ this.getOld();
+ }
+ },
+ });
+ },
+ };
+
+ window.Pager = Pager;
+})();
diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6
index e6fada5c84c..0b09ad113a3 100644
--- a/app/assets/javascripts/pipelines.js.es6
+++ b/app/assets/javascripts/pipelines.js.es6
@@ -1,29 +1,25 @@
-/* eslint-disable */
+/* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, padded-blocks, no-param-reassign, max-len */
+
+//= require lib/utils/bootstrap_linked_tabs
+
((global) => {
class Pipelines {
- constructor() {
- this.initGraphToggle();
- this.addMarginToBuildColumns();
- }
+ constructor(options = {}) {
- initGraphToggle() {
- this.pipelineGraph = document.querySelector('.pipeline-graph');
- this.toggleButton = document.querySelector('.toggle-pipeline-btn');
- this.toggleButtonText = this.toggleButton.querySelector('.toggle-btn-text');
- this.toggleButton.addEventListener('click', this.toggleGraph.bind(this));
- }
+ if (options.initTabs && options.tabsOptions) {
+ new global.LinkedTabs(options.tabsOptions);
+ }
- toggleGraph() {
- const graphCollapsed = this.pipelineGraph.classList.contains('graph-collapsed');
- this.toggleButton.classList.toggle('graph-collapsed');
- this.pipelineGraph.classList.toggle('graph-collapsed');
- this.toggleButtonText.textContent = graphCollapsed ? 'Hide' : 'Expand';
+ this.addMarginToBuildColumns();
}
addMarginToBuildColumns() {
+ this.pipelineGraph = document.querySelector('.js-pipeline-graph');
+
const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)');
- for (buildNodeIndex in secondChildBuildNodes) {
+
+ for (const buildNodeIndex in secondChildBuildNodes) {
const buildNode = secondChildBuildNodes[buildNodeIndex];
const firstChildBuildNode = buildNode.previousElementSibling;
if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue;
@@ -35,6 +31,7 @@
const columnBuilds = previousColumn.querySelectorAll('.build');
if (columnBuilds.length === 1) previousColumn.classList.add('no-margin');
}
+
this.pipelineGraph.classList.remove('hidden');
}
}
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index f2a45a18bed..1e261cd49c2 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, wrap-iife, no-else-return, consistent-return, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, camelcase, prefer-arrow-callback, max-len */
+
// MarkdownPreview
//
// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview,
@@ -7,7 +8,7 @@
(function() {
var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector;
- this.MarkdownPreview = (function() {
+ window.MarkdownPreview = (function() {
function MarkdownPreview() {}
// Minimum number of users referenced before triggering a warning
@@ -27,7 +28,7 @@
return this.renderMarkdown(mdText, (function(_this) {
return function(response) {
preview.html(response.body);
- preview.syntaxHighlight();
+ preview.renderGFM();
return _this.renderReferencedUsers(response.references.users, form);
};
})(this));
@@ -83,7 +84,7 @@
})();
- markdownPreview = new MarkdownPreview();
+ markdownPreview = new window.MarkdownPreview();
previewButtonSelector = '.js-md-preview-button';
diff --git a/app/assets/javascripts/profile/gl_crop.js.es6 b/app/assets/javascripts/profile/gl_crop.js.es6
index 6da6c1d0295..b4b6da41f63 100644
--- a/app/assets/javascripts/profile/gl_crop.js.es6
+++ b/app/assets/javascripts/profile/gl_crop.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-useless-escape, max-len, padded-blocks, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, no-plusplus, new-parens, semi */
+
((global) => {
// Matches everything but the file name
diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6
index 73858388261..eb2fe3477a2 100644
--- a/app/assets/javascripts/profile/profile.js.es6
+++ b/app/assets/javascripts/profile/profile.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len, padded-blocks */
+/* global Flash */
+
((global) => {
class Profile {
@@ -35,7 +37,6 @@
}
onSubmitForm(e) {
- e.preventDefault();
return this.saveForm();
}
diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js
index 22bee0f6187..f50802bdf2e 100644
--- a/app/assets/javascripts/profile/profile_bundle.js
+++ b/app/assets/javascripts/profile/profile_bundle.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
/*= require_tree . */
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 2d0c6b16699..fcf3a4af956 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, semi, vars-on-top, indent, prefer-template, padded-blocks, max-len */
+/* global Cookies */
+/* global Turbolinks */
+/* global ProjectSelect */
+
(function() {
this.Project = (function() {
function Project() {
diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js
index 61877c6616d..84f28ede4bf 100644
--- a/app/assets/javascripts/project_avatar.js
+++ b/app/assets/javascripts/project_avatar.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-useless-escape, padded-blocks, max-len */
(function() {
this.ProjectAvatar = (function() {
function ProjectAvatar() {
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index ddac5ed83e1..1bd232314d0 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, no-plusplus, prefer-template, no-unused-vars, no-return-assign, padded-blocks */
+/* global fuzzaldrinPlus */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js
index fd95f8f2c19..4aedc9a2330 100644
--- a/app/assets/javascripts/project_fork.js
+++ b/app/assets/javascripts/project_fork.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, padded-blocks, max-len */
(function() {
this.ProjectFork = (function() {
function ProjectFork() {
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js
index f1c4a9fe542..02dafcfb865 100644
--- a/app/assets/javascripts/project_import.js
+++ b/app/assets/javascripts/project_import.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, padded-blocks, max-len */
+/* global Turbolinks */
+
(function() {
this.ProjectImport = (function() {
function ProjectImport() {
diff --git a/app/assets/javascripts/project_label_subscription.js.es6 b/app/assets/javascripts/project_label_subscription.js.es6
new file mode 100644
index 00000000000..b8d6a198996
--- /dev/null
+++ b/app/assets/javascripts/project_label_subscription.js.es6
@@ -0,0 +1,54 @@
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, prefer-const, max-len, no-param-reassign, padded-blocks */
+
+(function(global) {
+ class ProjectLabelSubscription {
+ constructor(container) {
+ this.$container = $(container);
+ this.$buttons = this.$container.find('.js-subscribe-button');
+
+ this.$buttons.on('click', this.toggleSubscription.bind(this));
+ }
+
+ toggleSubscription(event) {
+ event.preventDefault();
+
+ const $btn = $(event.currentTarget);
+ const $span = $btn.find('span');
+ const url = $btn.attr('data-url');
+ const oldStatus = $btn.attr('data-status');
+
+ $btn.addClass('disabled');
+ $span.toggleClass('hidden');
+
+ $.ajax({
+ type: 'POST',
+ url: url
+ }).done(() => {
+ let newStatus, newAction;
+
+ if (oldStatus === 'unsubscribed') {
+ [newStatus, newAction] = ['subscribed', 'Unsubscribe'];
+ } else {
+ [newStatus, newAction] = ['unsubscribed', 'Subscribe'];
+ }
+
+ $span.toggleClass('hidden');
+ $btn.removeClass('disabled');
+
+ this.$buttons.attr('data-status', newStatus);
+ this.$buttons.find('> span').text(newAction);
+
+ for (let button of this.$buttons) {
+ let $button = $(button);
+
+ if ($button.attr('data-original-title')) {
+ $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
+ }
+ }
+ });
+ }
+ }
+
+ global.ProjectLabelSubscription = ProjectLabelSubscription;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js
index 0d3fb31a9cf..7fc611d0dad 100644
--- a/app/assets/javascripts/project_new.js
+++ b/app/assets/javascripts/project_new.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-vars, one-var, indent, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, radix, padded-blocks, max-len */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index e1acf3c8232..650996700ba 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, padded-blocks, max-len */
+/* global Api */
+
(function() {
this.ProjectSelect = (function() {
function ProjectSelect() {
diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js
index 21650f5f67a..eaf4c03d573 100644
--- a/app/assets/javascripts/project_show.js
+++ b/app/assets/javascripts/project_show.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, padded-blocks */
(function() {
this.ProjectShow = (function() {
function ProjectShow() {}
diff --git a/app/assets/javascripts/project_variables.js.es6 b/app/assets/javascripts/project_variables.js.es6
new file mode 100644
index 00000000000..4ee2e49306d
--- /dev/null
+++ b/app/assets/javascripts/project_variables.js.es6
@@ -0,0 +1,43 @@
+(() => {
+ const HIDDEN_VALUE_TEXT = '******';
+
+ class ProjectVariables {
+ constructor() {
+ this.$revealBtn = $('.js-btn-toggle-reveal-values');
+ this.$revealBtn.on('click', this.toggleRevealState.bind(this));
+ }
+
+ toggleRevealState(e) {
+ e.preventDefault();
+
+ const oldStatus = this.$revealBtn.attr('data-status');
+ let newStatus = 'hidden';
+ let newAction = 'Reveal Values';
+
+ if (oldStatus === 'hidden') {
+ newStatus = 'revealed';
+ newAction = 'Hide Values';
+ }
+
+ this.$revealBtn.attr('data-status', newStatus);
+
+ const $variables = $('.variable-value');
+
+ $variables.each((_, variable) => {
+ const $variable = $(variable);
+ let newText = HIDDEN_VALUE_TEXT;
+
+ if (newStatus === 'revealed') {
+ newText = $variable.attr('data-value');
+ }
+
+ $variable.text(newText);
+ });
+
+ this.$revealBtn.text(newAction);
+ }
+ }
+
+ window.gl = window.gl || {};
+ window.gl.ProjectVariables = ProjectVariables;
+})();
diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js
index 3458cd89ae2..4548dc68fe1 100644
--- a/app/assets/javascripts/projects_list.js
+++ b/app/assets/javascripts/projects_list.js
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, padded-blocks, max-len */
+
(function() {
- this.ProjectsList = {
+ window.ProjectsList = {
init: function() {
$(".projects-list-filter").off('keyup');
this.initSearch();
@@ -9,7 +10,7 @@
initSearch: function() {
var debounceFilter, projectsListFilter;
projectsListFilter = $('.projects-list-filter');
- debounceFilter = _.debounce(ProjectsList.filterResults, 500);
+ debounceFilter = _.debounce(window.ProjectsList.filterResults, 500);
return projectsListFilter.on('keyup', function(e) {
if (projectsListFilter.val() !== '') {
return debounceFilter();
diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
index 2d60947a666..4aef1c84b56 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable arrow-parens, no-param-reassign, no-irregular-whitespace, object-shorthand, no-else-return, comma-dangle, semi, padded-blocks, max-len */
+
(global => {
global.gl = global.gl || {};
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
index c45c9d8ff22..f26fba979a4 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable no-new, arrow-parens, no-param-reassign, no-irregular-whitespace, comma-dangle, padded-blocks, semi, max-len */
+/* global ProtectedBranchDropdown */
+
(global => {
global.gl = global.gl || {};
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 e3f226e9a2a..08264ad3d2f 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, no-unused-vars */
+
class ProtectedBranchDropdown {
constructor(options) {
this.onSelect = options.onSelect;
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
index ac3142ffb07..4ff2fa5a80f 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable no-new, arrow-parens, no-param-reassign, no-irregular-whitespace, padded-blocks, comma-dangle, no-trailing-spaces, semi, max-len */
+/* global Flash */
+
(global => {
global.gl = global.gl || {};
@@ -33,7 +35,7 @@
const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`);
// Do not update if one dropdown has not selected any option
- if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
+ if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
$.ajax({
type: 'POST',
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
index 705378a364d..b6972ef2e16 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable arrow-parens, no-param-reassign, no-irregular-whitespace, no-new, comma-dangle, semi, padded-blocks, max-len */
+
(global => {
global.gl = global.gl || {};
diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
index 17e34163831..15b3affd469 100644
--- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js
+++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
@@ -1,2 +1 @@
-/* eslint-disable */
/*= require_tree . */
diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js
new file mode 100644
index 00000000000..bbb2f186655
--- /dev/null
+++ b/app/assets/javascripts/render_gfm.js
@@ -0,0 +1,16 @@
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
+// Render Gitlab flavoured Markdown
+//
+// Delegates to syntax highlight and render math
+//
+(function() {
+ $.fn.renderGFM = function() {
+ this.find('.js-syntax-highlight').syntaxHighlight();
+ this.find('.js-render-math').renderMath();
+ };
+
+ $(document).on('ready page:load', function() {
+ return $('body').renderGFM();
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js
new file mode 100644
index 00000000000..209e7a8661d
--- /dev/null
+++ b/app/assets/javascripts/render_math.js
@@ -0,0 +1,55 @@
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len, no-console */
+// Renders math using KaTeX in any element with the
+// `js-render-math` class
+//
+// ### Example Markup
+//
+// <code class="js-render-math"></div>
+//
+(function() {
+ // Only load once
+ var katexLoaded = false;
+
+ // Loop over all math elements and render math
+ var renderWithKaTeX = function (elements) {
+ elements.each(function () {
+ var mathNode = $('<span></span>');
+ var $this = $(this);
+
+ var display = $this.attr('data-math-style') === 'display';
+ try {
+ katex.render($this.text(), mathNode.get(0), { displayMode: display });
+ mathNode.insertAfter($this);
+ $this.remove();
+ } catch (err) {
+ // What can we do??
+ console.log(err.message);
+ }
+ });
+ };
+
+ $.fn.renderMath = function() {
+ var $this = this;
+ if ($this.length === 0) return;
+
+ if (katexLoaded) renderWithKaTeX($this);
+ else {
+ // Request CSS file so it is in the cache
+ $.get(gon.katex_css_url, function() {
+ var css = $('<link>',
+ { rel: 'stylesheet',
+ type: 'text/css',
+ href: gon.katex_css_url,
+ });
+ css.appendTo('head');
+
+ // Load KaTeX js
+ $.getScript(gon.katex_js_url, function() {
+ katexLoaded = true;
+ renderWithKaTeX($this); // Run KaTeX
+ });
+ });
+ }
+ };
+
+}).call(this);
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index df38937858f..b1e844b7302 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-vars, semi, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, padded-blocks, max-len */
+/* global Cookies */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
index d79e6f014f6..4b6ebadeac7 100644
--- a/app/assets/javascripts/search.js
+++ b/app/assets/javascripts/search.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, padded-blocks, max-len */
+/* global Api */
+
(function() {
this.Search = (function() {
function Search() {
diff --git a/app/assets/javascripts/search_autocomplete.js.es6 b/app/assets/javascripts/search_autocomplete.js.es6
index 5fa94556501..437f5dbbf7d 100644
--- a/app/assets/javascripts/search_autocomplete.js.es6
+++ b/app/assets/javascripts/search_autocomplete.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, no-plusplus, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, padded-blocks, no-extra-semi, indent, max-len */
+
((global) => {
const KEYCODE = {
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index 8d8ab6dda5e..5ea00f408f4 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-plusplus, no-else-return, comma-dangle, padded-blocks, max-len */
+/* global Mousetrap */
+/* global Turbolinks */
+/* global findFileURL */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js
index 704a8bd3a57..c26903038b4 100644
--- a/app/assets/javascripts/shortcuts_blob.js
+++ b/app/assets/javascripts/shortcuts_blob.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return, padded-blocks */
+/* global Shortcuts */
+/* global Mousetrap */
/*= require shortcuts */
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js
index befe4eccdba..4549742bbcb 100644
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks */
+/* global Mousetrap */
+/* global Shortcuts */
/*= require shortcuts */
diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js
index 90ed4267661..3a81380eef0 100644
--- a/app/assets/javascripts/shortcuts_find_file.js
+++ b/app/assets/javascripts/shortcuts_find_file.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks */
+/* global Mousetrap */
+/* global ShortcutsNavigation */
/*= require shortcuts_navigation */
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index 25ec7dbc067..b892fbc3393 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators, padded-blocks */
+/* global Mousetrap */
+/* global Turbolinks */
+/* global ShortcutsNavigation */
+/* global sidebar */
/*= require mousetrap */
/*= require shortcuts_navigation */
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index 19c6b7d30ab..0776d0a9b67 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks */
+/* global Mousetrap */
+/* global Shortcuts */
/*= require shortcuts */
diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js
index 002e979a2c6..ecc3fab84c3 100644
--- a/app/assets/javascripts/shortcuts_network.js
+++ b/app/assets/javascripts/shortcuts_network.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks, max-len */
+/* global Mousetrap */
+/* global ShortcutsNavigation */
/*= require shortcuts_navigation */
diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6
index a23ca449c4b..9790a44972d 100644
--- a/app/assets/javascripts/sidebar.js.es6
+++ b/app/assets/javascripts/sidebar.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign, padded-blocks */
+/* global Cookies */
+
((global) => {
let singleton;
diff --git a/app/assets/javascripts/signin_tabs_memoizer.js.es6 b/app/assets/javascripts/signin_tabs_memoizer.js.es6
new file mode 100644
index 00000000000..d811d1cd53a
--- /dev/null
+++ b/app/assets/javascripts/signin_tabs_memoizer.js.es6
@@ -0,0 +1,49 @@
+/* eslint no-param-reassign: ["error", { "props": false }]*/
+/* eslint no-new: "off" */
+((global) => {
+ /**
+ * Memorize the last selected tab after reloading a page.
+ * Does that setting the current selected tab in the localStorage
+ */
+ class ActiveTabMemoizer {
+ constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) {
+ this.currentTabKey = currentTabKey;
+ this.tabSelector = tabSelector;
+ this.bootstrap();
+ }
+
+ bootstrap() {
+ const tabs = document.querySelectorAll(this.tabSelector);
+ if (tabs.length > 0) {
+ tabs[0].addEventListener('click', (e) => {
+ if (e.target && e.target.nodeName === 'A') {
+ const anchorName = e.target.getAttribute('href');
+ this.saveData(anchorName);
+ }
+ });
+ }
+
+ this.showTab();
+ }
+
+ showTab() {
+ const anchorName = this.readData();
+ if (anchorName) {
+ const tab = document.querySelector(`${this.tabSelector} a[href="${anchorName}"]`);
+ if (tab) {
+ tab.click();
+ }
+ }
+ }
+
+ saveData(val) {
+ localStorage.setItem(this.currentTabKey, val);
+ }
+
+ readData() {
+ return localStorage.getItem(this.currentTabKey);
+ }
+ }
+
+ global.ActiveTabMemoizer = ActiveTabMemoizer;
+})(window);
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 8e54ca4f0dc..ac8603ccd10 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -1,8 +1,9 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, padded-blocks, max-len */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
- this.SingleFileDiff = (function() {
+ window.SingleFileDiff = (function() {
var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
WRAPPER = '<div class="diff-content diff-wrap-lines"></div>';
@@ -14,6 +15,7 @@
COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
function SingleFileDiff(file, forceLoad, cb) {
+ var clickTarget;
this.file = file;
this.toggleDiff = bind(this.toggleDiff, this);
this.content = $('.diff-content', this.file);
@@ -31,9 +33,9 @@
this.content.after(this.collapsedContent);
this.$toggleIcon.addClass('fa-caret-down');
}
- $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff);
+ clickTarget = $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff);
if (forceLoad) {
- this.toggleDiff(null, cb);
+ this.toggleDiff({ target: clickTarget }, cb);
}
}
@@ -45,15 +47,15 @@
this.content.hide();
this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down');
this.collapsedContent.show();
- if (typeof DiffNotesApp !== 'undefined') {
- DiffNotesApp.compileComponents();
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
}
} else if (this.content) {
this.collapsedContent.hide();
this.content.show();
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
- if (typeof DiffNotesApp !== 'undefined') {
- DiffNotesApp.compileComponents();
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
}
} else {
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
@@ -76,8 +78,8 @@
}
_this.collapsedContent.after(_this.content);
- if (typeof DiffNotesApp !== 'undefined') {
- DiffNotesApp.compileComponents();
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
}
if (cb) cb();
@@ -92,7 +94,7 @@
$.fn.singleFileDiff = function(forceLoad, cb) {
return this.each(function() {
if (!$.data(this, 'singleFileDiff') || forceLoad) {
- return $.data(this, 'singleFileDiff', new SingleFileDiff(this, forceLoad, cb));
+ return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this, forceLoad, cb));
}
});
};
diff --git a/app/assets/javascripts/smart_interval.js.es6 b/app/assets/javascripts/smart_interval.js.es6
new file mode 100644
index 00000000000..40f67637c7c
--- /dev/null
+++ b/app/assets/javascripts/smart_interval.js.es6
@@ -0,0 +1,157 @@
+/*
+* Instances of SmartInterval extend the functionality of `setInterval`, make it configurable
+* and controllable by a public API.
+*
+* */
+
+(() => {
+ class SmartInterval {
+ /**
+ * @param { function } opts.callback Function to be called on each iteration (required)
+ * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially
+ * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this
+ * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this
+ * when the page is hidden
+ * @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor
+ * @param { boolean } opts.lazyStart Configure if timer is initialized on
+ * instantiation or lazily
+ * @param { boolean } opts.immediateExecution Configure if callback should
+ * be executed before the first interval.
+ */
+ constructor(opts = {}) {
+ this.cfg = {
+ callback: opts.callback,
+ startingInterval: opts.startingInterval,
+ maxInterval: opts.maxInterval,
+ hiddenInterval: opts.hiddenInterval,
+ incrementByFactorOf: opts.incrementByFactorOf,
+ lazyStart: opts.lazyStart,
+ immediateExecution: opts.immediateExecution,
+ };
+
+ this.state = {
+ intervalId: null,
+ currentInterval: this.cfg.startingInterval,
+ pageVisibility: 'visible',
+ };
+
+ this.initInterval();
+ }
+ /* public */
+
+ start() {
+ const cfg = this.cfg;
+ const state = this.state;
+
+ if (cfg.immediateExecution) {
+ cfg.immediateExecution = false;
+ cfg.callback();
+ }
+
+ state.intervalId = window.setInterval(() => {
+ cfg.callback();
+
+ if (this.getCurrentInterval() === cfg.maxInterval) {
+ return;
+ }
+
+ this.incrementInterval();
+ this.resume();
+ }, this.getCurrentInterval());
+ }
+
+ // cancel the existing timer, setting the currentInterval back to startingInterval
+ cancel() {
+ this.setCurrentInterval(this.cfg.startingInterval);
+ this.stopTimer();
+ }
+
+ onVisibilityHidden() {
+ if (this.cfg.hiddenInterval) {
+ this.setCurrentInterval(this.cfg.hiddenInterval);
+ this.resume();
+ } else {
+ this.cancel();
+ }
+ }
+
+ // start a timer, using the existing interval
+ resume() {
+ this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
+ this.start();
+ }
+
+ onVisibilityVisible() {
+ this.cancel();
+ this.start();
+ }
+
+ destroy() {
+ this.cancel();
+ document.removeEventListener('visibilitychange', this.handleVisibilityChange);
+ $(document).off('visibilitychange').off('page:before-unload');
+ }
+
+ /* private */
+
+ initInterval() {
+ const cfg = this.cfg;
+
+ if (!cfg.lazyStart) {
+ this.start();
+ }
+
+ this.initVisibilityChangeHandling();
+ this.initPageUnloadHandling();
+ }
+
+ initVisibilityChangeHandling() {
+ // cancel interval when tab no longer shown (prevents cached pages from polling)
+ document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
+ }
+
+ initPageUnloadHandling() {
+ // prevent interval continuing after page change, when kept in cache by Turbolinks
+ $(document).on('page:before-unload', () => this.cancel());
+ }
+
+ handleVisibilityChange(e) {
+ this.state.pageVisibility = e.target.visibilityState;
+ const intervalAction = this.isPageVisible() ?
+ this.onVisibilityVisible :
+ this.onVisibilityHidden;
+
+ intervalAction.apply(this);
+ }
+
+ getCurrentInterval() {
+ return this.state.currentInterval;
+ }
+
+ setCurrentInterval(newInterval) {
+ this.state.currentInterval = newInterval;
+ }
+
+ incrementInterval() {
+ const cfg = this.cfg;
+ const currentInterval = this.getCurrentInterval();
+ if (cfg.hiddenInterval && !this.isPageVisible()) return;
+ let nextInterval = currentInterval * cfg.incrementByFactorOf;
+
+ if (nextInterval > cfg.maxInterval) {
+ nextInterval = cfg.maxInterval;
+ }
+
+ this.setCurrentInterval(nextInterval);
+ }
+
+ isPageVisible() { return this.state.pageVisibility === 'visible'; }
+
+ stopTimer() {
+ const state = this.state;
+
+ state.intervalId = window.clearInterval(state.intervalId);
+ }
+ }
+ gl.SmartInterval = SmartInterval;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
index 083dc23c796..18512d179b3 100644
--- a/app/assets/javascripts/snippet/snippet_bundle.js
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, semi, padded-blocks, max-len */
+/* global ace */
+
/*= require_tree . */
(function() {
diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js.es6
index c3afc3f2246..6f913326a3a 100644
--- a/app/assets/javascripts/snippets_list.js.es6
+++ b/app/assets/javascripts/snippets_list.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, semi, max-len */
+
(global => {
global.gl = global.gl || {};
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index cfd1e2204d5..f1fc526bf2e 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, padded-blocks, max-len */
+/* global Flash */
+
(function() {
this.Star = (function() {
function Star() {
diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6
new file mode 100644
index 00000000000..932120157a3
--- /dev/null
+++ b/app/assets/javascripts/subbable_resource.js.es6
@@ -0,0 +1,54 @@
+//= require vue
+//= require vue-resource
+
+(() => {
+/*
+* SubbableResource can be extended to provide a pubsub-style service for one-off REST
+* calls. Subscribe by passing a callback or render method you will use to handle responses.
+ *
+* */
+
+ class SubbableResource {
+ constructor(resourcePath) {
+ this.endpoint = resourcePath;
+
+ // TODO: Switch to axios.create
+ this.resource = $.ajax;
+ this.subscribers = [];
+ }
+
+ subscribe(callback) {
+ this.subscribers.push(callback);
+ }
+
+ publish(newResponse) {
+ const responseCopy = _.extend({}, newResponse);
+ this.subscribers.forEach((fn) => {
+ fn(responseCopy);
+ });
+ return newResponse;
+ }
+
+ get(payload) {
+ return this.resource(payload)
+ .then(data => this.publish(data));
+ }
+
+ post(payload) {
+ return this.resource(payload)
+ .then(data => this.publish(data));
+ }
+
+ put(payload) {
+ return this.resource(payload)
+ .then(data => this.publish(data));
+ }
+
+ delete(payload) {
+ return this.resource(payload)
+ .then(data => this.publish(data));
+ }
+ }
+
+ gl.SubbableResource = SubbableResource;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js
deleted file mode 100644
index f9915593657..00000000000
--- a/app/assets/javascripts/subscription.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/* eslint-disable */
-(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
- this.Subscription = (function() {
- function Subscription(container) {
- this.toggleSubscription = bind(this.toggleSubscription, this);
- var $container;
- this.$container = $(container);
- this.url = this.$container.attr('data-url');
- this.subscribe_button = this.$container.find('.js-subscribe-button');
- this.subscription_status = this.$container.find('.subscription-status');
- this.subscribe_button.unbind('click').click(this.toggleSubscription);
- }
-
- Subscription.prototype.toggleSubscription = function(event) {
- var action, btn, current_status;
- btn = $(event.currentTarget);
- action = btn.find('span').text();
- current_status = this.subscription_status.attr('data-status');
- btn.addClass('disabled');
-
- if ($('html').hasClass('issue-boards-page')) {
- this.url = this.$container.attr('data-url');
- }
-
- return $.post(this.url, (function(_this) {
- return function() {
- var status;
- btn.removeClass('disabled');
-
- if ($('html').hasClass('issue-boards-page')) {
- Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'subscribed', !gl.issueBoards.BoardsStore.detail.issue.subscribed);
- } else {
- status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed';
- _this.subscription_status.attr('data-status', status);
- action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe';
- btn.find('span').text(action);
- _this.subscription_status.find('>div').toggleClass('hidden');
- if (btn.attr('data-original-title')) {
- return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle');
- }
- }
- };
- })(this));
- };
-
- return Subscription;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/subscription.js.es6 b/app/assets/javascripts/subscription.js.es6
new file mode 100644
index 00000000000..62d1604fe9e
--- /dev/null
+++ b/app/assets/javascripts/subscription.js.es6
@@ -0,0 +1,50 @@
+/* global Vue */
+
+(() => {
+ class Subscription {
+ constructor(containerElm) {
+ this.containerElm = containerElm;
+
+ const subscribeButton = containerElm.querySelector('.js-subscribe-button');
+ if (subscribeButton) {
+ // remove class so we don't bind twice
+ subscribeButton.classList.remove('js-subscribe-button');
+ subscribeButton.addEventListener('click', this.toggleSubscription.bind(this));
+ }
+ }
+
+ toggleSubscription(event) {
+ const button = event.currentTarget;
+ const buttonSpan = button.querySelector('span');
+ if (!buttonSpan || button.classList.contains('disabled')) {
+ return;
+ }
+ button.classList.add('disabled');
+
+ const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe';
+ const toggleActionUrl = this.containerElm.dataset.url;
+
+ $.post(toggleActionUrl, () => {
+ button.classList.remove('disabled');
+
+ // hack to allow this to work with the issue boards Vue object
+ if (document.querySelector('html').classList.contains('issue-boards-page')) {
+ Vue.set(
+ gl.issueBoards.BoardsStore.detail.issue,
+ 'subscribed',
+ !gl.issueBoards.BoardsStore.detail.issue.subscribed,
+ );
+ } else {
+ buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe';
+ }
+ });
+ }
+
+ static bindAll(selector) {
+ [].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm));
+ }
+ }
+
+ window.gl = window.gl || {};
+ window.gl.Subscription = Subscription;
+})();
diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js
index 2ca65cb762d..185d20775d0 100644
--- a/app/assets/javascripts/subscription_select.js
+++ b/app/assets/javascripts/subscription_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */
(function() {
this.SubscriptionSelect = (function() {
function SubscriptionSelect() {
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index 77ad4f30b7a..5d0fa62c50a 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
+
// Syntax Highlighter
//
// Applies a syntax highlighting color scheme CSS class to any element with the
@@ -9,8 +10,10 @@
// <div class="js-syntax-highlight"></div>
//
(function() {
+
$.fn.syntaxHighlight = function() {
var $children;
+
if ($(this).hasClass('js-syntax-highlight')) {
// Given the element itself, apply highlighting
return $(this).addClass(gon.user_color_scheme);
@@ -23,8 +26,4 @@
}
};
- $(document).on('ready page:load', function() {
- return $('.js-syntax-highlight').syntaxHighlight();
- });
-
}).call(this);
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6
index 93a3d67ee9f..d2b152045b4 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable prefer-const, comma-dangle, max-len, no-useless-return, object-curly-spacing, no-param-reassign, max-len */
+/* global Api */
+
/*= require ../blob/template_selector */
((global) => {
diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
index 0a3890e85fe..7310b9de074 100644
--- a/app/assets/javascripts/templates/issuable_template_selectors.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-new, comma-dangle, class-methods-use-this, prefer-const, no-param-reassign */
+
((global) => {
class IssuableTemplateSelectors {
constructor({ $dropdowns, editor } = {}) {
diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6
index 213e80825b7..d8713600030 100644
--- a/app/assets/javascripts/todos.js.es6
+++ b/app/assets/javascripts/todos.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable padded-blocks, class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, semi, no-param-reassign, max-len */
+/* global UsersSelect */
+/* global Turbolinks */
+
((global) => {
class Todos {
@@ -72,7 +75,7 @@
allDoneClicked(e) {
e.preventDefault();
e.stopImmediatePropagation();
- $target = $(e.currentTarget);
+ const $target = $(e.currentTarget);
$target.disable();
return $.ajax({
type: 'POST',
diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js
index 70aff4b9a2f..f48a7ee0f55 100644
--- a/app/assets/javascripts/tree.js
+++ b/app/assets/javascripts/tree.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
+/* global Turbolinks */
(function() {
this.TreeView = (function() {
function TreeView() {
diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js
index 35f2b1e2b25..d2aa3c7a841 100644
--- a/app/assets/javascripts/u2f/authenticate.js
+++ b/app/assets/javascripts/u2f/authenticate.js
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */
+/* global u2f */
+/* global U2FError */
+/* global U2FUtil */
+
// Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
//
// State Flow #1: setup -> in_progress -> authenticated -> POST to server
diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js
index aff605169e4..69f98c9c0ad 100644
--- a/app/assets/javascripts/u2f/error.js
+++ b/app/assets/javascripts/u2f/error.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, padded-blocks, max-len */
+/* global u2f */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js
index 22fbf9f3a91..4f5d68f546b 100644
--- a/app/assets/javascripts/u2f/register.js
+++ b/app/assets/javascripts/u2f/register.js
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */
+/* global u2f */
+/* global U2FError */
+/* global U2FUtil */
+
// Register U2F (universal 2nd factor) devices for users to authenticate with.
//
// State Flow #1: setup -> in_progress -> registered -> POST to server
diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js
index 2eab2d5ae23..eedd3bcd5a1 100644
--- a/app/assets/javascripts/u2f/util.js
+++ b/app/assets/javascripts/u2f/util.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, padded-blocks */
(function() {
this.U2FUtil = (function() {
function U2FUtil() {}
diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6
index 5e869e99fdb..0a2db7c05fe 100644
--- a/app/assets/javascripts/user.js.es6
+++ b/app/assets/javascripts/user.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign, semi */
+/* global Cookies */
+
((global) => {
global.User = class {
constructor({ action }) {
diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6
index 2b310da319c..b9c23b51b4d 100644
--- a/app/assets/javascripts/user_tabs.js.es6
+++ b/app/assets/javascripts/user_tabs.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, array-bracket-spacing, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, semi, no-param-reassign */
+
/*
UserTabs
@@ -134,7 +135,7 @@ content on the Users#show page.
}
const $calendarWrap = this.$parentEl.find('.user-calendar');
$calendarWrap.load($calendarWrap.data('href'));
- new Activities();
+ new gl.Activities();
return this.loaded['activity'] = true;
}
diff --git a/app/assets/javascripts/username_validator.js.es6 b/app/assets/javascripts/username_validator.js.es6
index c4dde575c6e..137cefa3b8e 100644
--- a/app/assets/javascripts/username_validator.js.es6
+++ b/app/assets/javascripts/username_validator.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */
+
((global) => {
const debounceTimeoutDuration = 1000;
const invalidInputClass = 'gl-field-error-outline';
@@ -77,7 +78,7 @@
this.renderState();
return $.ajax({
type: 'GET',
- url: `/users/${username}/exists`,
+ url: `${gon.relative_url_root}/users/${username}/exists`,
dataType: 'json',
success: (res) => this.setAvailabilityState(res.exists)
});
diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js
index 0ec878e7e60..578be7c3590 100644
--- a/app/assets/javascripts/users/calendar.js
+++ b/app/assets/javascripts/users/calendar.js
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, vars-on-top, semi, keyword-spacing, no-plusplus, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, padded-blocks, max-len */
+/* global d3 */
+/* global dateFormat */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -206,6 +209,7 @@
}
});
} else {
+ this.currentSelectedDate = '';
return $('.user-calendar-activities').html('');
}
};
diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js
index 22bee0f6187..f50802bdf2e 100644
--- a/app/assets/javascripts/users/users_bundle.js
+++ b/app/assets/javascripts/users/users_bundle.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
/*= require_tree . */
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 7a2221dbaf5..d4b5e03aa35 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-plusplus, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, keyword-spacing, no-param-reassign, padded-blocks */
+/* global Vue */
+/* global Issuable */
+/* global ListUser */
+
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
slice = [].slice;
diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6
new file mode 100644
index 00000000000..62a22e39a3b
--- /dev/null
+++ b/app/assets/javascripts/vue_common_component/commit.js.es6
@@ -0,0 +1,163 @@
+/*= require vue */
+/* global Vue */
+(() => {
+ window.gl = window.gl || {};
+
+ window.gl.CommitComponent = Vue.component('commit-component', {
+
+ props: {
+ /**
+ * Indicates the existance of a tag.
+ * Used to render the correct icon, if true will render `fa-tag` icon,
+ * if false will render `fa-code-fork` icon.
+ */
+ tag: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ /**
+ * If provided is used to render the branch name and url.
+ * Should contain the following properties:
+ * name
+ * ref_url
+ */
+ commitRef: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+
+ /**
+ * Used to link to the commit sha.
+ */
+ commitUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * Used to show the commit short sha that links to the commit url.
+ */
+ shortSha: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * If provided shows the commit tile.
+ */
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * If provided renders information about the author of the commit.
+ * When provided should include:
+ * `avatar_url` to render the avatar icon
+ * `web_url` to link to user profile
+ * `username` to render alt and title tags
+ */
+ author: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+
+ commitIconSvg: {
+ type: String,
+ required: false,
+ },
+ },
+
+ computed: {
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * ref section were provided.
+ *
+ * TODO: Improve this! Use lodash _.has when we have it.
+ *
+ * @returns {Boolean}
+ */
+ hasCommitRef() {
+ return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
+ },
+
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * author section were provided.
+ *
+ * TODO: Improve this! Use lodash _.has when we have it.
+ *
+ * @returns {Boolean}
+ */
+ hasAuthor() {
+ return this.author &&
+ this.author.avatar_url &&
+ this.author.web_url &&
+ this.author.username;
+ },
+
+ /**
+ * If information about the author is provided will return a string
+ * to be rendered as the alt attribute of the img tag.
+ *
+ * @returns {String}
+ */
+ userImageAltDescription() {
+ return this.author &&
+ this.author.username ? `${this.author.username}'s avatar` : null;
+ },
+ },
+
+ template: `
+ <div class="branch-commit">
+
+ <div v-if="hasCommitRef" class="icon-container">
+ <i v-if="tag" class="fa fa-tag"></i>
+ <i v-if="!tag" class="fa fa-code-fork"></i>
+ </div>
+
+ <a v-if="hasCommitRef"
+ class="monospace branch-name"
+ :href="commitRef.ref_url">
+ {{commitRef.name}}
+ </a>
+
+ <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
+
+ <a class="commit-id monospace"
+ :href="commitUrl">
+ {{shortSha}}
+ </a>
+
+ <p class="commit-title">
+ <span v-if="title">
+ <a v-if="hasAuthor"
+ class="avatar-image-container"
+ :href="author.web_url">
+ <img
+ class="avatar has-tooltip s20"
+ :src="author.avatar_url"
+ :alt="userImageAltDescription"
+ :title="author.username" />
+ </a>
+
+ <a class="commit-row-message"
+ :href="commitUrl">
+ {{title}}
+ </a>
+ </span>
+ <span v-else>
+ Cant find HEAD commit for this branch
+ </span>
+ </p>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js
deleted file mode 100644
index ad9b842db3c..00000000000
--- a/app/assets/javascripts/wikis.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/* eslint-disable */
-
-/*= require latinise */
-
-(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
- this.Wikis = (function() {
- function Wikis() {
- this.slugify = bind(this.slugify, this);
- $('.new-wiki-page').on('submit', (function(_this) {
- return function(e) {
- var field, path, slug;
- $('[data-error~=slug]').addClass('hidden');
- field = $('#new_wiki_path');
- slug = _this.slugify(field.val());
- if (slug.length > 0) {
- path = field.attr('data-wikis-path');
- location.href = path + '/' + slug;
- return e.preventDefault();
- }
- };
- })(this));
- }
-
- Wikis.prototype.dasherize = function(value) {
- return value.replace(/[_\s]+/g, '-');
- };
-
- Wikis.prototype.slugify = function(value) {
- return this.dasherize(value.trim().toLowerCase().latinise());
- };
-
- return Wikis;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6
new file mode 100644
index 00000000000..ecff5fd5bf4
--- /dev/null
+++ b/app/assets/javascripts/wikis.js.es6
@@ -0,0 +1,73 @@
+/* eslint-disable no-param-reassign */
+/* global Breakpoints */
+
+/*= require latinise */
+/*= require breakpoints */
+/*= require jquery.nicescroll */
+
+((global) => {
+ const dasherize = str => str.replace(/[_\s]+/g, '-');
+ const slugify = str => dasherize(str.trim().toLowerCase().latinise());
+
+ class Wikis {
+ constructor() {
+ this.bp = Breakpoints.get();
+ this.sidebarEl = document.querySelector('.js-wiki-sidebar');
+ this.sidebarExpanded = false;
+ $(this.sidebarEl).niceScroll();
+
+ const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle');
+ for (let i = 0; i < sidebarToggles.length; i += 1) {
+ sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e));
+ }
+
+ this.newWikiForm = document.querySelector('form.new-wiki-page');
+ if (this.newWikiForm) {
+ this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e));
+ }
+
+ window.addEventListener('resize', () => this.renderSidebar());
+ this.renderSidebar();
+ }
+
+ handleNewWikiSubmit(e) {
+ if (!this.newWikiForm) return;
+
+ const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
+ const slug = slugify(slugInput.value);
+
+ if (slug.length > 0) {
+ const wikisPath = slugInput.getAttribute('data-wikis-path');
+ window.location.href = `${wikisPath}/${slug}`;
+ e.preventDefault();
+ }
+ }
+
+ handleToggleSidebar(e) {
+ e.preventDefault();
+ this.sidebarExpanded = !this.sidebarExpanded;
+ this.renderSidebar();
+ }
+
+ sidebarCanCollapse() {
+ const bootstrapBreakpoint = this.bp.getBreakpointSize();
+ return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
+ }
+
+ renderSidebar() {
+ if (!this.sidebarEl) return;
+ const { classList } = this.sidebarEl;
+ if (this.sidebarExpanded || !this.sidebarCanCollapse()) {
+ if (!classList.contains('right-sidebar-expanded')) {
+ classList.remove('right-sidebar-collapsed');
+ classList.add('right-sidebar-expanded');
+ }
+ } else if (classList.contains('right-sidebar-expanded')) {
+ classList.add('right-sidebar-collapsed');
+ classList.remove('right-sidebar-expanded');
+ }
+ }
+ }
+
+ global.Wikis = Wikis;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index fa124e7052d..e09b59dd5aa 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, padded-blocks, max-len */
+/* global Dropzone */
+/* global Mousetrap */
+
// Zen Mode (full screen) textarea
//
/*= provides zen_mode:enter */
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index d5cca1b10fb..40bc0579393 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -1,4 +1,3 @@
-@import "framework/fonts";
@import "framework/variables";
@import "framework/mixins";
@import 'framework/tw_bootstrap_variables';
@@ -7,6 +6,7 @@
@import "framework/animations.scss";
@import "framework/avatar.scss";
+@import "framework/asciidoctor.scss";
@import "framework/blocks.scss";
@import "framework/buttons.scss";
@import "framework/calendar.scss";
@@ -39,3 +39,11 @@
@import "framework/typography.scss";
@import "framework/zen.scss";
@import "framework/blank";
+@import "framework/wells.scss";
+@import "framework/page-header.scss";
+@import "framework/awards.scss";
+@import "framework/images.scss";
+@import "framework/broadcast-messages";
+@import "framework/emojis.scss";
+@import "framework/icons.scss";
+@import "framework/snippets.scss";
diff --git a/app/assets/stylesheets/framework/asciidoctor.scss b/app/assets/stylesheets/framework/asciidoctor.scss
new file mode 100644
index 00000000000..62493c32833
--- /dev/null
+++ b/app/assets/stylesheets/framework/asciidoctor.scss
@@ -0,0 +1,27 @@
+.admonitionblock td.icon {
+ width: 1%;
+
+ [class^="fa icon-"] {
+ @extend .fa-2x;
+ }
+
+ .icon-note {
+ @extend .fa-thumb-tack;
+ }
+
+ .icon-tip {
+ @extend .fa-lightbulb-o;
+ }
+
+ .icon-warning {
+ @extend .fa-exclamation-triangle;
+ }
+
+ .icon-caution {
+ @extend .fa-fire;
+ }
+
+ .icon-important {
+ @extend .fa-exclamation-circle;
+ }
+}
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 202ed5ae8fe..000e591e09c 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -8,7 +8,7 @@
float: left;
margin-right: 15px;
border-radius: $avatar_radius;
- border: 1px solid rgba(0, 0, 0, .1);
+ border: 1px solid $avatar-border;
&.s16 { @include avatar-size(16px, 6px); }
&.s20 { @include avatar-size(20px, 7px); }
&.s24 { @include avatar-size(24px, 8px); }
@@ -34,6 +34,7 @@
&.avatar-inline {
float: none;
+ display: inline-block;
margin-left: 4px;
margin-bottom: 2px;
@@ -41,6 +42,12 @@
&.s24 { margin-right: 4px; }
}
+ &.center {
+ font-size: 14px;
+ line-height: 1.8em;
+ text-align: center;
+ }
+
&.avatar-tile {
border-radius: 0;
border: none;
@@ -73,6 +80,7 @@
border-radius: 0;
border: none;
height: auto;
+ width: 100%;
margin: 0;
align-self: center;
}
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
new file mode 100644
index 00000000000..9fc9bcebc44
--- /dev/null
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -0,0 +1,141 @@
+.awards {
+ .emoji-icon {
+ width: 20px;
+ height: 20px;
+ }
+}
+
+.emoji-menu {
+ position: absolute;
+ margin-top: 3px;
+ padding: $gl-padding;
+ z-index: 9;
+ width: 300px;
+ font-size: 14px;
+ background-color: $white-light;
+ border: 1px solid $border-white-light;
+ border-radius: $border-radius-base;
+ box-shadow: 0 6px 12px $award-emoji-menu-shadow;
+ pointer-events: none;
+ opacity: 0;
+ transform: scale(.2);
+ transform-origin: 0 -45px;
+ transition: .3s cubic-bezier(.87,-.41,.19,1.44);
+ transition-property: transform, opacity;
+
+ &.is-aligned-right {
+ transform-origin: 100% -45px;
+ }
+
+ &.is-visible {
+ pointer-events: all;
+ opacity: 1;
+ transform: scale(1);
+ }
+
+ .emoji-menu-content {
+ height: 300px;
+ overflow-y: scroll;
+ }
+}
+
+.emoji-search {
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC");
+ background-repeat: no-repeat;
+ background-position: right 5px center;
+ background-size: 16px;
+}
+
+.emoji-menu-list {
+ list-style: none;
+ padding-left: 0;
+ margin-bottom: 0;
+}
+
+.emoji-menu-list-item {
+ padding: 3px;
+ margin-left: 1px;
+ margin-right: 1px;
+}
+
+.emoji-menu-btn {
+ display: block;
+ cursor: pointer;
+ width: 30px;
+ height: 30px;
+ padding: 0;
+ background: none;
+ border: 0;
+ border-radius: $border-radius-base;
+ transition: transform .15s cubic-bezier(.3, 0, .2, 2);
+
+ &:hover {
+ background-color: transparent;
+ outline: 0;
+ transform: scale(1.3);
+ }
+
+ &:focus,
+ &:active {
+ outline: 0;
+ }
+
+ .emoji-icon {
+ display: inline-block;
+ position: relative;
+ top: 3px;
+ }
+}
+
+.award-menu-holder {
+ display: inline-block;
+ position: relative;
+}
+
+.award-control {
+ margin: 3px 5px 3px 0;
+ padding: 5px 6px;
+ outline: 0;
+
+ &:hover,
+ &.active,
+ &:active {
+ background-color: $row-hover;
+ border-color: $row-hover-border;
+ box-shadow: none;
+ outline: 0;
+ }
+
+ &.btn {
+ &:focus {
+ outline: 0;
+ }
+ }
+
+ &.is-loading {
+ .award-control-icon-normal,
+ .emoji-icon {
+ display: none;
+ }
+
+ .award-control-icon-loading {
+ display: block;
+ }
+ }
+
+ .icon,
+ .award-control-icon {
+ float: left;
+ margin-right: 5px;
+ font-size: 18px;
+ }
+
+ .award-control-icon-loading {
+ display: none;
+ }
+
+ .award-control-icon {
+ color: $border-gray-normal;
+ margin-top: 1px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss
index 540718197e0..a2fa2e7769b 100644
--- a/app/assets/stylesheets/framework/blank.scss
+++ b/app/assets/stylesheets/framework/blank.scss
@@ -32,14 +32,14 @@
.blank-state-title {
margin-top: 0;
margin-bottom: 5px;
- font-size: 19px;
+ font-size: 18px;
font-weight: normal;
}
.blank-state-text {
margin-top: 0;
margin-bottom: $gl-padding;
- font-size: 15px;
+ font-size: 14px;
> strong {
font-weight: 600;
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 7e168092522..9f02749f5ab 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -1,8 +1,3 @@
-.light-well {
- background-color: $background-color;
- padding: 15px;
-}
-
.centered-light-block {
text-align: center;
color: $gl-gray;
@@ -29,7 +24,7 @@
.row-content-block {
margin-top: 0;
margin-bottom: -$gl-padding;
- background-color: $background-color;
+ background-color: $gray-light;
padding: $gl-padding;
margin-bottom: 0;
border-top: 1px solid $white-dark;
@@ -41,7 +36,7 @@
}
&.white {
- background-color: white;
+ background-color: $white-light;
}
&.top-block {
@@ -123,7 +118,7 @@
.cover-block {
text-align: center;
- background: $background-color;
+ background: $gray-light;
padding-top: 44px;
position: relative;
@@ -158,7 +153,7 @@
p {
padding: 0 $gl-padding;
- color: #5c5d5e;
+ color: $gl-text-color-dark;
}
}
@@ -254,3 +249,36 @@
.content-block-small {
padding: 10px 0;
}
+
+.empty-state {
+ margin: 100px 0 0;
+
+ .text-content {
+ max-width: 460px;
+ margin: 0 auto;
+ padding: $gl-padding;
+ }
+
+ .svg-content {
+ text-align: center;
+
+ svg {
+ max-width: 425px;
+ width: 100%;
+ padding: $gl-padding;
+ }
+ }
+
+ .emoji-icon {
+ display: inline-block;
+ }
+
+ @media(max-width: $screen-xs-max) {
+ margin-top: 50px;
+ text-align: center;
+
+ .btn {
+ width: 100%;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/broadcast-messages.scss b/app/assets/stylesheets/framework/broadcast-messages.scss
new file mode 100644
index 00000000000..9b54fb94cdc
--- /dev/null
+++ b/app/assets/stylesheets/framework/broadcast-messages.scss
@@ -0,0 +1,21 @@
+.broadcast-message {
+ @extend .alert-warning;
+ padding: 10px;
+ text-align: center;
+
+ div,
+ p {
+ display: inline;
+ margin: 0;
+
+ a {
+ color: inherit;
+ text-decoration: underline;
+ }
+ }
+}
+
+.broadcast-message-preview {
+ @extend .broadcast-message;
+ margin-bottom: 20px;
+}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index ed21ad83a1c..59ff17ad2c1 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -1,12 +1,11 @@
@mixin btn-default {
border-radius: 3px;
font-size: $gl-font-size;
- font-weight: 500;
+ font-weight: 400;
padding: $gl-vert-padding $gl-btn-padding;
&:focus,
&:active {
- outline: none;
background-color: $btn-active-gray;
box-shadow: $gl-btn-active-background;
}
@@ -16,7 +15,7 @@
@include btn-default;
}
-@mixin btn-outline($background, $text, $border, $hover-background, $hover-text, $hover-border) {
+@mixin btn-outline($background, $text, $border, $hover-background, $hover-text, $hover-border, $active-background, $active-border) {
background-color: $background;
color: $text;
border-color: $border;
@@ -24,8 +23,14 @@
&:hover,
&:focus {
background-color: $hover-background;
- color: $hover-text;
border-color: $hover-border;
+ color: $hover-text;
+ }
+
+ &:active {
+ background-color: $active-background;
+ border-color: $active-border;
+ color: $hover-text;
}
}
@@ -63,31 +68,31 @@
}
@mixin btn-green {
- @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, #fff);
+ @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, $white-light);
}
@mixin btn-blue {
- @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #fff);
+ @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, $white-light);
}
@mixin btn-blue-medium {
- @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, #fff);
+ @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, $white-light);
}
@mixin btn-orange {
- @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #fff);
+ @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, $white-light);
}
@mixin btn-red {
- @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, #fff);
+ @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, $white-light);
}
@mixin btn-gray {
- @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-light, $gray-dark, $border-gray-dark, $gl-gray-dark);
+ @include btn-color($gray-light, $border-gray-normal, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-gray-dark);
}
@mixin btn-white {
- @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $btn-white-active);
+ @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-gray-dark, $gl-text-color);
}
@mixin btn-with-margin {
@@ -140,7 +145,11 @@
&.btn-new,
&.btn-create,
&.btn-save {
- @include btn-outline($white-light, $green-normal, $green-normal, $green-light, $white-light, $green-light);
+ @include btn-outline($white-light, $border-green-light, $border-green-light, $green-light, $white-light, $border-green-light, $green-normal, $border-green-normal);
+ }
+
+ &.btn-remove {
+ @include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal);
}
}
@@ -162,11 +171,11 @@
}
&.btn-close {
- @include btn-outline($white-light, $orange-normal, $orange-normal, $orange-light, $white-light, $orange-light);
+ @include btn-outline($white-light, $border-orange-light, $border-orange-light, $orange-light, $white-light, $border-orange-light, $orange-normal, $border-orange-normal);
}
&.btn-spam {
- @include btn-outline($white-light, $red-normal, $red-normal, $red-light, $white-light, $red-light);
+ @include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal);
}
&.btn-danger,
@@ -196,7 +205,7 @@
}
.fa-caret-down,
- .fa-caret-up {
+ .fa-chevron-down {
margin-left: 5px;
}
@@ -226,7 +235,7 @@
}
.btn-transparent {
- color: $btn-transparent-color;
+ color: $gl-gray-light;
background-color: transparent;
border: 0;
@@ -267,10 +276,6 @@
outline: none;
}
- &:focus {
- outline: none;
- }
-
&:active {
outline: none;
}
@@ -284,8 +289,8 @@
.active {
box-shadow: $gl-btn-active-background;
- border: 1px solid #c6cacf !important;
- background-color: #e4e7ed !important;
+ border: 1px solid $border-gray-dark !important;
+ background-color: $btn-active-gray-light !important;
}
}
@@ -304,8 +309,8 @@
text-align: left;
padding: 6px 16px;
border-color: $border-color;
- color: $btn-placeholder-gray;
- background-color: $background-color;
+ color: $gray-darkest;
+ background-color: $gray-light;
&:hover,
&:active,
@@ -313,8 +318,8 @@
cursor: text;
box-shadow: none;
border-color: $border-color;
- color: $btn-placeholder-gray;
- background-color: $background-color;
+ color: $gray-darkest;
+ background-color: $gray-light;
}
}
@@ -326,7 +331,7 @@
margin-left: 10px;
i {
- color: $gl-icon-color;
+ color: $gl-gray-light;
}
}
@@ -339,14 +344,20 @@
}
.btn-static {
- background-color: $background-color !important;
- border: 1px solid lightgrey;
+ background-color: $gray-light !important;
+ border: 1px solid $border-gray-normal;
cursor: default;
&:active {
- -moz-box-shadow: inset 0 0 0 white;
- -webkit-box-shadow: inset 0 0 0 white;
- box-shadow: inset 0 0 0 white;
+ -moz-box-shadow: inset 0 0 0 $white-light;
+ -webkit-box-shadow: inset 0 0 0 $white-light;
+ box-shadow: inset 0 0 0 $white-light;
+ }
+}
+
+.btn-inverted {
+ &-secondary {
+ @include btn-outline($white-light, $border-blue-light, $border-blue-light, $blue-light, $white-light, $border-blue-light, $blue-normal, $border-blue-normal);
}
}
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index 8642b7530e2..ef921a8c6a9 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -2,7 +2,7 @@
padding-left: 0;
padding-right: 0;
- @media (min-width: $screen-sm-min) and (max-width: $screen-lg-min) {
+ @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
overflow-x: scroll;
}
}
@@ -28,13 +28,13 @@
.user-contrib-cell {
&:hover {
cursor: pointer;
- stroke: #000;
+ stroke: $black;
}
}
.user-contrib-text {
font-size: 12px;
- fill: #959494;
+ fill: $calendar-user-contrib-text;
}
.calendar-hint {
diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss
index f3b6ad88ad6..e0e46dd73af 100644
--- a/app/assets/stylesheets/framework/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -11,7 +11,7 @@
padding: $gl-padding;
border-left: 3px solid $border-color;
color: $text-color;
- background: $background-color;
+ background: $gray-light;
}
.bs-callout h4 {
@@ -25,25 +25,25 @@
/* Variations */
.bs-callout-danger {
- background-color: #fdf7f7;
- border-color: #eed3d7;
- color: #b94a48;
+ background-color: $callout-danger-bg;
+ border-color: $callout-danger-border;
+ color: $callout-danger-color;
}
.bs-callout-warning {
- background-color: #faf8f0;
- border-color: #faebcc;
- color: #8a6d3b;
+ background-color: $callout-warning-bg;
+ border-color: $callout-warning-border;
+ color: $callout-warning-color;
}
.bs-callout-info {
- background-color: #f4f8fa;
- border-color: #bce8f1;
- color: #34789a;
+ background-color: $callout-info-bg;
+ border-color: $callout-info-border;
+ color: $callout-info-color;
}
.bs-callout-success {
- background-color: #dff0d8;
- border-color: #5ca64d;
- color: #3c763d;
+ background-color: $callout-success-bg;
+ border-color: $callout-success-border;
+ color: $callout-success-color;
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index ad5ac589d0f..251e43d2edd 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -1,9 +1,9 @@
/** COLORS **/
-.cgray { color: $gl-gray; }
-.clgray { color: #bbb; }
-.cred { color: $gl-text-red; }
-.cgreen { color: $gl-text-green; }
-.cdark { color: #444; }
+.cgray { color: $common-gray; }
+.clgray { color: $common-gray-light; }
+.cred { color: $common-red; }
+.cgreen { color: $common-green; }
+.cdark { color: $common-gray-dark; }
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
@@ -28,12 +28,12 @@
.center { text-align: center; }
.underlined-link { text-decoration: underline; }
-.hint { font-style: italic; color: #999; }
-.light { color: $gl-gray; }
+.hint { font-style: italic; color: $hint-color; }
+.light { color: $common-gray; }
.slead {
- color: $gl-gray;
- font-size: 15px;
+ color: $common-gray;
+ font-size: 14px;
margin-bottom: 12px;
font-weight: normal;
line-height: 24px;
@@ -52,10 +52,10 @@ pre {
}
&.well-pre {
- border: 1px solid #eee;
+ border: 1px solid $well-pre-bg;
background: $gray-light;
border-radius: 0;
- color: #555;
+ color: $well-pre-color;
}
}
@@ -87,14 +87,14 @@ table a code {
.loading {
margin: 20px auto;
height: 40px;
- color: #555;
+ color: $loading-color;
font-size: 32px;
text-align: center;
}
span.update-author {
display: block;
- color: #999;
+ color: $update-author-color;
font-weight: normal;
font-style: italic;
@@ -105,7 +105,7 @@ span.update-author {
}
.user-mention {
- color: #2fa0bb;
+ color: $user-mention-color;
font-weight: bold;
}
@@ -114,7 +114,7 @@ span.update-author {
}
p.time {
- color: #999;
+ color: $time-color;
font-size: 90%;
margin: 30px 3px 3px 2px;
}
@@ -150,7 +150,7 @@ li.note {
.project_member_show {
td:first-child {
- color: #aaa;
+ color: $project-member-show-color;
}
}
@@ -176,7 +176,7 @@ li.note {
margin-top: 40px;
pre {
- background: white;
+ background: $white-light;
border: none;
font-size: 12px;
}
@@ -184,12 +184,12 @@ li.note {
.error-message {
padding: 10px;
- background: #c67;
+ background: $error-bg;
margin: 0;
- color: #fff;
+ color: $white-light;
a {
- color: #fff;
+ color: $white-light;
text-decoration: underline;
}
}
@@ -197,22 +197,22 @@ li.note {
.browser-alert {
padding: 10px;
text-align: center;
- background: #c67;
- color: #fff;
+ background: $error-bg;
+ color: $white-light;
font-weight: bold;
a {
- color: #fff;
+ color: $white-light;
text-decoration: underline;
}
}
.warning_message {
- border-left: 4px solid #ed9;
- color: #b90;
+ border-left: 4px solid $warning-message-border;
+ color: $warning-message-color;
padding: 10px;
margin-bottom: 10px;
- background: #ffffe6;
+ background: $warning-message-bg;
padding-left: 20px;
&.centered {
@@ -222,7 +222,7 @@ li.note {
.gitlab-promo {
a {
- color: #aaa;
+ color: $gl-promo-color;
margin-right: 30px;
}
}
@@ -245,7 +245,7 @@ li.note {
position: relative;
top: 2px;
left: 5px;
- color: #666;
+ color: $control-group-descr-color;
}
}
}
@@ -255,6 +255,7 @@ img.emoji {
height: 20px;
vertical-align: top;
width: 20px;
+ margin-top: 1px;
}
.chart {
@@ -270,7 +271,7 @@ img.emoji {
table {
td.permission-x {
- background: #d9edf7 !important;
+ background: $table-permission-x-bg !important;
text-align: center;
}
}
@@ -299,6 +300,10 @@ table {
.well {
margin-bottom: $gl-padding;
+
+ hr {
+ border-color: $gray-darker;
+ }
}
.search_box {
@@ -319,13 +324,13 @@ table {
.username {
font-size: 18px;
- color: #666;
+ color: $username-color;
margin-top: 8px;
}
.description {
font-size: $gl-font-size;
- color: #666;
+ color: $description-color;
margin-top: 8px;
}
}
@@ -335,7 +340,7 @@ table {
.profiler-button,
.profiler-controls {
- border-color: #eee !important;
+ border-color: $profiler-border !important;
}
}
@@ -375,4 +380,32 @@ table {
border-top: 1px solid $border-color;
}
-.hide-bottom-border { border-bottom: none !important; }
+.hide-bottom-border {
+ border-bottom: none !important;
+}
+
+.gl-accessibility {
+ &:focus {
+ top: 1px;
+ left: 1px;
+ width: auto;
+ height: 100%;
+ line-height: 50px;
+ padding: 0 10px;
+ clip: auto;
+ text-decoration: none;
+ color: $gl-title-color;
+ background: $gray-light;
+ z-index: 1;
+ }
+}
+
+.str-truncated {
+ &-60 {
+ @include str-truncated(60%);
+ }
+
+ &-100 {
+ @include str-truncated(100%);
+ }
+}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 3e34ec98427..889366d6ddf 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -8,6 +8,12 @@
}
}
+@mixin chevron-active {
+ .fa-chevron-down {
+ color: $gray-darkest;
+ }
+}
+
.open {
.dropdown-menu,
.dropdown-menu-nav {
@@ -19,48 +25,30 @@
}
}
+ .dropdown-toggle,
.dropdown-menu-toggle {
- border-color: $dropdown-toggle-hover-border-color;
-
- .fa {
- color: $dropdown-toggle-hover-icon-color;
- }
+ @include chevron-active;
+ border-color: $gray-darkest;
}
}
-.dropdown-menu-toggle {
- position: relative;
- width: 160px;
- padding: 6px 20px 6px 10px;
- background-color: $dropdown-toggle-bg;
- color: $dropdown-toggle-color;
- font-size: 15px;
+.dropdown-toggle {
+ padding: 6px 8px 6px 10px;
+ background-color: $white-light;
+ color: $gl-text-color;
+ font-size: 14px;
text-align: left;
border: 1px solid $border-color;
border-radius: $border-radius-base;
- outline: 0;
- text-overflow: ellipsis;
white-space: nowrap;
- overflow: hidden;
- .fa {
- position: absolute;
- top: 10px;
- right: 8px;
- color: $dropdown-toggle-icon-color;
-
- &.fa-spinner {
- font-size: 16px;
- margin-top: -8px;
- }
+ &[disabled] {
+ background-color: $input-bg-disabled;
+ cursor: not-allowed;
}
- &:hover, {
- border-color: $dropdown-toggle-hover-border-color;
-
- .fa {
- color: $dropdown-toggle-hover-icon-color;
- }
+ &.no-outline {
+ outline: 0;
}
&.large {
@@ -83,6 +71,51 @@
max-width: 100%;
padding-right: 25px;
}
+
+ .fa {
+ color: $gray-darkest;
+ }
+
+ .fa-chevron-down {
+ font-size: $dropdown-chevron-size;
+ position: relative;
+ top: -3px;
+ margin-left: 5px;
+ }
+
+ &:hover {
+ @include chevron-active;
+ border-color: $gray-darkest;
+ }
+
+ &:focus:active {
+ @include chevron-active;
+ border-color: $dropdown-toggle-active-border-color;
+ }
+}
+
+.dropdown-menu-toggle {
+ @extend .dropdown-toggle;
+ padding-right: 20px;
+ position: relative;
+ width: 163px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+
+ .fa {
+ position: absolute;
+
+ &.fa-spinner {
+ font-size: 16px;
+ margin-top: -8px;
+ }
+ }
+
+ .fa-chevron-down {
+ position: absolute;
+ top: 11px;
+ right: 8px;
+ }
}
.dropdown-menu,
@@ -95,10 +128,10 @@
width: 240px;
margin-top: 2px;
margin-bottom: 0;
- font-size: 15px;
+ font-size: 14px;
font-weight: normal;
padding: 8px 0;
- background-color: $dropdown-bg;
+ background-color: $white-light;
border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color;
@@ -155,7 +188,6 @@
&.is-focused {
background-color: $dropdown-link-hover-bg;
text-decoration: none;
- outline: 0;
}
&.dropdown-menu-empty-link {
@@ -169,7 +201,7 @@
}
.icon-play {
- fill: $table-text-gray;
+ fill: $gl-gray-light;
margin-right: 6px;
height: 12px;
width: 11px;
@@ -177,7 +209,7 @@
}
.dropdown-header {
- color: $dropdown-header-color;
+ color: $gl-gray-light;
font-size: 13px;
line-height: 22px;
padding: 0 10px;
@@ -190,7 +222,7 @@
.unclickable {
cursor: not-allowed;
padding: 5px 8px;
- color: $dropdown-header-color;
+ color: $gl-gray-light;
}
}
@@ -348,7 +380,7 @@
position: absolute;
top: 10px;
right: 20px;
- color: #c7c7c7;
+ color: $dropdown-input-fa-color;
font-size: 12px;
pointer-events: none;
}
@@ -501,7 +533,7 @@
.ui-datepicker-calendar {
.ui-state-hover,
.ui-state-active {
- color: #fff;
+ color: $white-light;
border: 0;
}
}
@@ -561,7 +593,7 @@
.ui-datepicker-title {
color: $gl-gray;
- font-size: 15px;
+ font-size: 14px;
line-height: 1;
font-weight: normal;
}
@@ -569,14 +601,14 @@
th {
padding: 2px 0;
- color: $calendar-header-color;
+ color: $note-disabled-comment-color;
font-weight: normal;
text-transform: lowercase;
border-top: 1px solid $calendar-border-color;
}
.ui-datepicker-unselectable {
- background-color: $calendar-unselectable-bg;
+ background-color: $gray-light;
}
}
@@ -588,11 +620,11 @@
.dropdown-menu-inner-content {
display: block;
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
}
.dropdown-toggle-text {
&.is-default {
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
}
}
diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
new file mode 100644
index 00000000000..7158de65143
--- /dev/null
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -0,0 +1,1809 @@
+.emoji-0023-20E3 { background-position: 0 0; }
+.emoji-002A-20E3 { background-position: -20px 0; }
+.emoji-0030-20E3 { background-position: 0 -20px; }
+.emoji-0031-20E3 { background-position: -20px -20px; }
+.emoji-0032-20E3 { background-position: -40px 0; }
+.emoji-0033-20E3 { background-position: -40px -20px; }
+.emoji-0034-20E3 { background-position: 0 -40px; }
+.emoji-0035-20E3 { background-position: -20px -40px; }
+.emoji-0036-20E3 { background-position: -40px -40px; }
+.emoji-0037-20E3 { background-position: -60px 0; }
+.emoji-0038-20E3 { background-position: -60px -20px; }
+.emoji-0039-20E3 { background-position: -60px -40px; }
+.emoji-00A9 { background-position: 0 -60px; }
+.emoji-00AE { background-position: -20px -60px; }
+.emoji-1F004 { background-position: -40px -60px; }
+.emoji-1F0CF { background-position: -60px -60px; }
+.emoji-1F170 { background-position: -80px 0; }
+.emoji-1F171 { background-position: -80px -20px; }
+.emoji-1F17E { background-position: -80px -40px; }
+.emoji-1F17F { background-position: -80px -60px; }
+.emoji-1F18E { background-position: 0 -80px; }
+.emoji-1F191 { background-position: -20px -80px; }
+.emoji-1F192 { background-position: -40px -80px; }
+.emoji-1F193 { background-position: -60px -80px; }
+.emoji-1F194 { background-position: -80px -80px; }
+.emoji-1F195 { background-position: -100px 0; }
+.emoji-1F196 { background-position: -100px -20px; }
+.emoji-1F197 { background-position: -100px -40px; }
+.emoji-1F198 { background-position: -100px -60px; }
+.emoji-1F199 { background-position: -100px -80px; }
+.emoji-1F19A { background-position: 0 -100px; }
+.emoji-1F1E6-1F1E8 { background-position: -20px -100px; }
+.emoji-1F1E6-1F1E9 { background-position: -40px -100px; }
+.emoji-1F1E6-1F1EA { background-position: -60px -100px; }
+.emoji-1F1E6-1F1EB { background-position: -80px -100px; }
+.emoji-1F1E6-1F1EC { background-position: -100px -100px; }
+.emoji-1F1E6-1F1EE { background-position: -120px 0; }
+.emoji-1F1E6-1F1F1 { background-position: -120px -20px; }
+.emoji-1F1E6-1F1F2 { background-position: -120px -40px; }
+.emoji-1F1E6-1F1F4 { background-position: -120px -60px; }
+.emoji-1F1E6-1F1F6 { background-position: -120px -80px; }
+.emoji-1F1E6-1F1F7 { background-position: -120px -100px; }
+.emoji-1F1E6-1F1F8 { background-position: 0 -120px; }
+.emoji-1F1E6-1F1F9 { background-position: -20px -120px; }
+.emoji-1F1E6-1F1FA { background-position: -40px -120px; }
+.emoji-1F1E6-1F1FC { background-position: -60px -120px; }
+.emoji-1F1E6-1F1FD { background-position: -80px -120px; }
+.emoji-1F1E6-1F1FF { background-position: -100px -120px; }
+.emoji-1F1E7-1F1E6 { background-position: -120px -120px; }
+.emoji-1F1E7-1F1E7 { background-position: -140px 0; }
+.emoji-1F1E7-1F1E9 { background-position: -140px -20px; }
+.emoji-1F1E7-1F1EA { background-position: -140px -40px; }
+.emoji-1F1E7-1F1EB { background-position: -140px -60px; }
+.emoji-1F1E7-1F1EC { background-position: -140px -80px; }
+.emoji-1F1E7-1F1ED { background-position: -140px -100px; }
+.emoji-1F1E7-1F1EE { background-position: -140px -120px; }
+.emoji-1F1E7-1F1EF { background-position: 0 -140px; }
+.emoji-1F1E7-1F1F1 { background-position: -20px -140px; }
+.emoji-1F1E7-1F1F2 { background-position: -40px -140px; }
+.emoji-1F1E7-1F1F3 { background-position: -60px -140px; }
+.emoji-1F1E7-1F1F4 { background-position: -80px -140px; }
+.emoji-1F1E7-1F1F6 { background-position: -100px -140px; }
+.emoji-1F1E7-1F1F7 { background-position: -120px -140px; }
+.emoji-1F1E7-1F1F8 { background-position: -140px -140px; }
+.emoji-1F1E7-1F1F9 { background-position: -160px 0; }
+.emoji-1F1E7-1F1FB { background-position: -160px -20px; }
+.emoji-1F1E7-1F1FC { background-position: -160px -40px; }
+.emoji-1F1E7-1F1FE { background-position: -160px -60px; }
+.emoji-1F1E7-1F1FF { background-position: -160px -80px; }
+.emoji-1F1E8-1F1E6 { background-position: -160px -100px; }
+.emoji-1F1E8-1F1E8 { background-position: -160px -120px; }
+.emoji-1F1E8-1F1E9 { background-position: -160px -140px; }
+.emoji-1F1E8-1F1EB { background-position: 0 -160px; }
+.emoji-1F1E8-1F1EC { background-position: -20px -160px; }
+.emoji-1F1E8-1F1ED { background-position: -40px -160px; }
+.emoji-1F1E8-1F1EE { background-position: -60px -160px; }
+.emoji-1F1E8-1F1F0 { background-position: -80px -160px; }
+.emoji-1F1E8-1F1F1 { background-position: -100px -160px; }
+.emoji-1F1E8-1F1F2 { background-position: -120px -160px; }
+.emoji-1F1E8-1F1F3 { background-position: -140px -160px; }
+.emoji-1F1E8-1F1F4 { background-position: -160px -160px; }
+.emoji-1F1E8-1F1F5 { background-position: -180px 0; }
+.emoji-1F1E8-1F1F7 { background-position: -180px -20px; }
+.emoji-1F1E8-1F1FA { background-position: -180px -40px; }
+.emoji-1F1E8-1F1FB { background-position: -180px -60px; }
+.emoji-1F1E8-1F1FC { background-position: -180px -80px; }
+.emoji-1F1E8-1F1FD { background-position: -180px -100px; }
+.emoji-1F1E8-1F1FE { background-position: -180px -120px; }
+.emoji-1F1E8-1F1FF { background-position: -180px -140px; }
+.emoji-1F1E9-1F1EA { background-position: -180px -160px; }
+.emoji-1F1E9-1F1EC { background-position: 0 -180px; }
+.emoji-1F1E9-1F1EF { background-position: -20px -180px; }
+.emoji-1F1E9-1F1F0 { background-position: -40px -180px; }
+.emoji-1F1E9-1F1F2 { background-position: -60px -180px; }
+.emoji-1F1E9-1F1F4 { background-position: -80px -180px; }
+.emoji-1F1E9-1F1FF { background-position: -100px -180px; }
+.emoji-1F1EA-1F1E6 { background-position: -120px -180px; }
+.emoji-1F1EA-1F1E8 { background-position: -140px -180px; }
+.emoji-1F1EA-1F1EA { background-position: -160px -180px; }
+.emoji-1F1EA-1F1EC { background-position: -180px -180px; }
+.emoji-1F1EA-1F1ED { background-position: -200px 0; }
+.emoji-1F1EA-1F1F7 { background-position: -200px -20px; }
+.emoji-1F1EA-1F1F8 { background-position: -200px -40px; }
+.emoji-1F1EA-1F1F9 { background-position: -200px -60px; }
+.emoji-1F1EA-1F1FA { background-position: -200px -80px; }
+.emoji-1F1EB-1F1EE { background-position: -200px -100px; }
+.emoji-1F1EB-1F1EF { background-position: -200px -120px; }
+.emoji-1F1EB-1F1F0 { background-position: -200px -140px; }
+.emoji-1F1EB-1F1F2 { background-position: -200px -160px; }
+.emoji-1F1EB-1F1F4 { background-position: -200px -180px; }
+.emoji-1F1EB-1F1F7 { background-position: 0 -200px; }
+.emoji-1F1EC-1F1E6 { background-position: -20px -200px; }
+.emoji-1F1EC-1F1E7 { background-position: -40px -200px; }
+.emoji-1F1EC-1F1E9 { background-position: -60px -200px; }
+.emoji-1F1EC-1F1EA { background-position: -80px -200px; }
+.emoji-1F1EC-1F1EB { background-position: -100px -200px; }
+.emoji-1F1EC-1F1EC { background-position: -120px -200px; }
+.emoji-1F1EC-1F1ED { background-position: -140px -200px; }
+.emoji-1F1EC-1F1EE { background-position: -160px -200px; }
+.emoji-1F1EC-1F1F1 { background-position: -180px -200px; }
+.emoji-1F1EC-1F1F2 { background-position: -200px -200px; }
+.emoji-1F1EC-1F1F3 { background-position: -220px 0; }
+.emoji-1F1EC-1F1F5 { background-position: -220px -20px; }
+.emoji-1F1EC-1F1F6 { background-position: -220px -40px; }
+.emoji-1F1EC-1F1F7 { background-position: -220px -60px; }
+.emoji-1F1EC-1F1F8 { background-position: -220px -80px; }
+.emoji-1F1EC-1F1F9 { background-position: -220px -100px; }
+.emoji-1F1EC-1F1FA { background-position: -220px -120px; }
+.emoji-1F1EC-1F1FC { background-position: -220px -140px; }
+.emoji-1F1EC-1F1FE { background-position: -220px -160px; }
+.emoji-1F1ED-1F1F0 { background-position: -220px -180px; }
+.emoji-1F1ED-1F1F2 { background-position: -220px -200px; }
+.emoji-1F1ED-1F1F3 { background-position: 0 -220px; }
+.emoji-1F1ED-1F1F7 { background-position: -20px -220px; }
+.emoji-1F1ED-1F1F9 { background-position: -40px -220px; }
+.emoji-1F1ED-1F1FA { background-position: -60px -220px; }
+.emoji-1F1EE-1F1E8 { background-position: -80px -220px; }
+.emoji-1F1EE-1F1E9 { background-position: -100px -220px; }
+.emoji-1F1EE-1F1EA { background-position: -120px -220px; }
+.emoji-1F1EE-1F1F1 { background-position: -140px -220px; }
+.emoji-1F1EE-1F1F2 { background-position: -160px -220px; }
+.emoji-1F1EE-1F1F3 { background-position: -180px -220px; }
+.emoji-1F1EE-1F1F4 { background-position: -200px -220px; }
+.emoji-1F1EE-1F1F6 { background-position: -220px -220px; }
+.emoji-1F1EE-1F1F7 { background-position: -240px 0; }
+.emoji-1F1EE-1F1F8 { background-position: -240px -20px; }
+.emoji-1F1EE-1F1F9 { background-position: -240px -40px; }
+.emoji-1F1EF-1F1EA { background-position: -240px -60px; }
+.emoji-1F1EF-1F1F2 { background-position: -240px -80px; }
+.emoji-1F1EF-1F1F4 { background-position: -240px -100px; }
+.emoji-1F1EF-1F1F5 { background-position: -240px -120px; }
+.emoji-1F1F0-1F1EA { background-position: -240px -140px; }
+.emoji-1F1F0-1F1EC { background-position: -240px -160px; }
+.emoji-1F1F0-1F1ED { background-position: -240px -180px; }
+.emoji-1F1F0-1F1EE { background-position: -240px -200px; }
+.emoji-1F1F0-1F1F2 { background-position: -240px -220px; }
+.emoji-1F1F0-1F1F3 { background-position: 0 -240px; }
+.emoji-1F1F0-1F1F5 { background-position: -20px -240px; }
+.emoji-1F1F0-1F1F7 { background-position: -40px -240px; }
+.emoji-1F1F0-1F1FC { background-position: -60px -240px; }
+.emoji-1F1F0-1F1FE { background-position: -80px -240px; }
+.emoji-1F1F0-1F1FF { background-position: -100px -240px; }
+.emoji-1F1F1-1F1E6 { background-position: -120px -240px; }
+.emoji-1F1F1-1F1E7 { background-position: -140px -240px; }
+.emoji-1F1F1-1F1E8 { background-position: -160px -240px; }
+.emoji-1F1F1-1F1EE { background-position: -180px -240px; }
+.emoji-1F1F1-1F1F0 { background-position: -200px -240px; }
+.emoji-1F1F1-1F1F7 { background-position: -220px -240px; }
+.emoji-1F1F1-1F1F8 { background-position: -240px -240px; }
+.emoji-1F1F1-1F1F9 { background-position: -260px 0; }
+.emoji-1F1F1-1F1FA { background-position: -260px -20px; }
+.emoji-1F1F1-1F1FB { background-position: -260px -40px; }
+.emoji-1F1F1-1F1FE { background-position: -260px -60px; }
+.emoji-1F1F2-1F1E6 { background-position: -260px -80px; }
+.emoji-1F1F2-1F1E8 { background-position: -260px -100px; }
+.emoji-1F1F2-1F1E9 { background-position: -260px -120px; }
+.emoji-1F1F2-1F1EA { background-position: -260px -140px; }
+.emoji-1F1F2-1F1EB { background-position: -260px -160px; }
+.emoji-1F1F2-1F1EC { background-position: -260px -180px; }
+.emoji-1F1F2-1F1ED { background-position: -260px -200px; }
+.emoji-1F1F2-1F1F0 { background-position: -260px -220px; }
+.emoji-1F1F2-1F1F1 { background-position: -260px -240px; }
+.emoji-1F1F2-1F1F2 { background-position: 0 -260px; }
+.emoji-1F1F2-1F1F3 { background-position: -20px -260px; }
+.emoji-1F1F2-1F1F4 { background-position: -40px -260px; }
+.emoji-1F1F2-1F1F5 { background-position: -60px -260px; }
+.emoji-1F1F2-1F1F6 { background-position: -80px -260px; }
+.emoji-1F1F2-1F1F7 { background-position: -100px -260px; }
+.emoji-1F1F2-1F1F8 { background-position: -120px -260px; }
+.emoji-1F1F2-1F1F9 { background-position: -140px -260px; }
+.emoji-1F1F2-1F1FA { background-position: -160px -260px; }
+.emoji-1F1F2-1F1FB { background-position: -180px -260px; }
+.emoji-1F1F2-1F1FC { background-position: -200px -260px; }
+.emoji-1F1F2-1F1FD { background-position: -220px -260px; }
+.emoji-1F1F2-1F1FE { background-position: -240px -260px; }
+.emoji-1F1F2-1F1FF { background-position: -260px -260px; }
+.emoji-1F1F3-1F1E6 { background-position: -280px 0; }
+.emoji-1F1F3-1F1E8 { background-position: -280px -20px; }
+.emoji-1F1F3-1F1EA { background-position: -280px -40px; }
+.emoji-1F1F3-1F1EB { background-position: -280px -60px; }
+.emoji-1F1F3-1F1EC { background-position: -280px -80px; }
+.emoji-1F1F3-1F1EE { background-position: -280px -100px; }
+.emoji-1F1F3-1F1F1 { background-position: -280px -120px; }
+.emoji-1F1F3-1F1F4 { background-position: -280px -140px; }
+.emoji-1F1F3-1F1F5 { background-position: -280px -160px; }
+.emoji-1F1F3-1F1F7 { background-position: -280px -180px; }
+.emoji-1F1F3-1F1FA { background-position: -280px -200px; }
+.emoji-1F1F3-1F1FF { background-position: -280px -220px; }
+.emoji-1F1F4-1F1F2 { background-position: -280px -240px; }
+.emoji-1F1F5-1F1E6 { background-position: -280px -260px; }
+.emoji-1F1F5-1F1EA { background-position: 0 -280px; }
+.emoji-1F1F5-1F1EB { background-position: -20px -280px; }
+.emoji-1F1F5-1F1EC { background-position: -40px -280px; }
+.emoji-1F1F5-1F1ED { background-position: -60px -280px; }
+.emoji-1F1F5-1F1F0 { background-position: -80px -280px; }
+.emoji-1F1F5-1F1F1 { background-position: -100px -280px; }
+.emoji-1F1F5-1F1F2 { background-position: -120px -280px; }
+.emoji-1F1F5-1F1F3 { background-position: -140px -280px; }
+.emoji-1F1F5-1F1F7 { background-position: -160px -280px; }
+.emoji-1F1F5-1F1F8 { background-position: -180px -280px; }
+.emoji-1F1F5-1F1F9 { background-position: -200px -280px; }
+.emoji-1F1F5-1F1FC { background-position: -220px -280px; }
+.emoji-1F1F5-1F1FE { background-position: -240px -280px; }
+.emoji-1F1F6-1F1E6 { background-position: -260px -280px; }
+.emoji-1F1F7-1F1EA { background-position: -280px -280px; }
+.emoji-1F1F7-1F1F4 { background-position: -300px 0; }
+.emoji-1F1F7-1F1F8 { background-position: -300px -20px; }
+.emoji-1F1F7-1F1FA { background-position: -300px -40px; }
+.emoji-1F1F7-1F1FC { background-position: -300px -60px; }
+.emoji-1F1F8-1F1E6 { background-position: -300px -80px; }
+.emoji-1F1F8-1F1E7 { background-position: -300px -100px; }
+.emoji-1F1F8-1F1E8 { background-position: -300px -120px; }
+.emoji-1F1F8-1F1E9 { background-position: -300px -140px; }
+.emoji-1F1F8-1F1EA { background-position: -300px -160px; }
+.emoji-1F1F8-1F1EC { background-position: -300px -180px; }
+.emoji-1F1F8-1F1ED { background-position: -300px -200px; }
+.emoji-1F1F8-1F1EE { background-position: -300px -220px; }
+.emoji-1F1F8-1F1EF { background-position: -300px -240px; }
+.emoji-1F1F8-1F1F0 { background-position: -300px -260px; }
+.emoji-1F1F8-1F1F1 { background-position: -300px -280px; }
+.emoji-1F1F8-1F1F2 { background-position: 0 -300px; }
+.emoji-1F1F8-1F1F3 { background-position: -20px -300px; }
+.emoji-1F1F8-1F1F4 { background-position: -40px -300px; }
+.emoji-1F1F8-1F1F7 { background-position: -60px -300px; }
+.emoji-1F1F8-1F1F8 { background-position: -80px -300px; }
+.emoji-1F1F8-1F1F9 { background-position: -100px -300px; }
+.emoji-1F1F8-1F1FB { background-position: -120px -300px; }
+.emoji-1F1F8-1F1FD { background-position: -140px -300px; }
+.emoji-1F1F8-1F1FE { background-position: -160px -300px; }
+.emoji-1F1F8-1F1FF { background-position: -180px -300px; }
+.emoji-1F1F9-1F1E6 { background-position: -200px -300px; }
+.emoji-1F1F9-1F1E8 { background-position: -220px -300px; }
+.emoji-1F1F9-1F1E9 { background-position: -240px -300px; }
+.emoji-1F1F9-1F1EB { background-position: -260px -300px; }
+.emoji-1F1F9-1F1EC { background-position: -280px -300px; }
+.emoji-1F1F9-1F1ED { background-position: -300px -300px; }
+.emoji-1F1F9-1F1EF { background-position: -320px 0; }
+.emoji-1F1F9-1F1F0 { background-position: -320px -20px; }
+.emoji-1F1F9-1F1F1 { background-position: -320px -40px; }
+.emoji-1F1F9-1F1F2 { background-position: -320px -60px; }
+.emoji-1F1F9-1F1F3 { background-position: -320px -80px; }
+.emoji-1F1F9-1F1F4 { background-position: -320px -100px; }
+.emoji-1F1F9-1F1F7 { background-position: -320px -120px; }
+.emoji-1F1F9-1F1F9 { background-position: -320px -140px; }
+.emoji-1F1F9-1F1FB { background-position: -320px -160px; }
+.emoji-1F1F9-1F1FC { background-position: -320px -180px; }
+.emoji-1F1F9-1F1FF { background-position: -320px -200px; }
+.emoji-1F1FA-1F1E6 { background-position: -320px -220px; }
+.emoji-1F1FA-1F1EC { background-position: -320px -240px; }
+.emoji-1F1FA-1F1F2 { background-position: -320px -260px; }
+.emoji-1F1FA-1F1F8 { background-position: -320px -280px; }
+.emoji-1F1FA-1F1FE { background-position: -320px -300px; }
+.emoji-1F1FA-1F1FF { background-position: 0 -320px; }
+.emoji-1F1FB-1F1E6 { background-position: -20px -320px; }
+.emoji-1F1FB-1F1E8 { background-position: -40px -320px; }
+.emoji-1F1FB-1F1EA { background-position: -60px -320px; }
+.emoji-1F1FB-1F1EC { background-position: -80px -320px; }
+.emoji-1F1FB-1F1EE { background-position: -100px -320px; }
+.emoji-1F1FB-1F1F3 { background-position: -120px -320px; }
+.emoji-1F1FB-1F1FA { background-position: -140px -320px; }
+.emoji-1F1FC-1F1EB { background-position: -160px -320px; }
+.emoji-1F1FC-1F1F8 { background-position: -180px -320px; }
+.emoji-1F1FD-1F1F0 { background-position: -200px -320px; }
+.emoji-1F1FE-1F1EA { background-position: -220px -320px; }
+.emoji-1F1FE-1F1F9 { background-position: -240px -320px; }
+.emoji-1F1FF-1F1E6 { background-position: -260px -320px; }
+.emoji-1F1FF-1F1F2 { background-position: -280px -320px; }
+.emoji-1F1FF-1F1FC { background-position: -300px -320px; }
+.emoji-1F201 { background-position: -320px -320px; }
+.emoji-1F202 { background-position: -340px 0; }
+.emoji-1F21A { background-position: -340px -20px; }
+.emoji-1F22F { background-position: -340px -40px; }
+.emoji-1F232 { background-position: -340px -60px; }
+.emoji-1F233 { background-position: -340px -80px; }
+.emoji-1F234 { background-position: -340px -100px; }
+.emoji-1F235 { background-position: -340px -120px; }
+.emoji-1F236 { background-position: -340px -140px; }
+.emoji-1F237 { background-position: -340px -160px; }
+.emoji-1F238 { background-position: -340px -180px; }
+.emoji-1F239 { background-position: -340px -200px; }
+.emoji-1F23A { background-position: -340px -220px; }
+.emoji-1F250 { background-position: -340px -240px; }
+.emoji-1F251 { background-position: -340px -260px; }
+.emoji-1F300 { background-position: -340px -280px; }
+.emoji-1F301 { background-position: -340px -300px; }
+.emoji-1F302 { background-position: -340px -320px; }
+.emoji-1F303 { background-position: 0 -340px; }
+.emoji-1F304 { background-position: -20px -340px; }
+.emoji-1F305 { background-position: -40px -340px; }
+.emoji-1F306 { background-position: -60px -340px; }
+.emoji-1F307 { background-position: -80px -340px; }
+.emoji-1F308 { background-position: -100px -340px; }
+.emoji-1F309 { background-position: -120px -340px; }
+.emoji-1F30A { background-position: -140px -340px; }
+.emoji-1F30B { background-position: -160px -340px; }
+.emoji-1F30C { background-position: -180px -340px; }
+.emoji-1F30D { background-position: -200px -340px; }
+.emoji-1F30E { background-position: -220px -340px; }
+.emoji-1F30F { background-position: -240px -340px; }
+.emoji-1F310 { background-position: -260px -340px; }
+.emoji-1F311 { background-position: -280px -340px; }
+.emoji-1F312 { background-position: -300px -340px; }
+.emoji-1F313 { background-position: -320px -340px; }
+.emoji-1F314 { background-position: -340px -340px; }
+.emoji-1F315 { background-position: -360px 0; }
+.emoji-1F316 { background-position: -360px -20px; }
+.emoji-1F317 { background-position: -360px -40px; }
+.emoji-1F318 { background-position: -360px -60px; }
+.emoji-1F319 { background-position: -360px -80px; }
+.emoji-1F31A { background-position: -360px -100px; }
+.emoji-1F31B { background-position: -360px -120px; }
+.emoji-1F31C { background-position: -360px -140px; }
+.emoji-1F31D { background-position: -360px -160px; }
+.emoji-1F31E { background-position: -360px -180px; }
+.emoji-1F31F { background-position: -360px -200px; }
+.emoji-1F320 { background-position: -360px -220px; }
+.emoji-1F321 { background-position: -360px -240px; }
+.emoji-1F324 { background-position: -360px -260px; }
+.emoji-1F325 { background-position: -360px -280px; }
+.emoji-1F326 { background-position: -360px -300px; }
+.emoji-1F327 { background-position: -360px -320px; }
+.emoji-1F328 { background-position: -360px -340px; }
+.emoji-1F329 { background-position: 0 -360px; }
+.emoji-1F32A { background-position: -20px -360px; }
+.emoji-1F32B { background-position: -40px -360px; }
+.emoji-1F32C { background-position: -60px -360px; }
+.emoji-1F32D { background-position: -80px -360px; }
+.emoji-1F32E { background-position: -100px -360px; }
+.emoji-1F32F { background-position: -120px -360px; }
+.emoji-1F330 { background-position: -140px -360px; }
+.emoji-1F331 { background-position: -160px -360px; }
+.emoji-1F332 { background-position: -180px -360px; }
+.emoji-1F333 { background-position: -200px -360px; }
+.emoji-1F334 { background-position: -220px -360px; }
+.emoji-1F335 { background-position: -240px -360px; }
+.emoji-1F336 { background-position: -260px -360px; }
+.emoji-1F337 { background-position: -280px -360px; }
+.emoji-1F338 { background-position: -300px -360px; }
+.emoji-1F339 { background-position: -320px -360px; }
+.emoji-1F33A { background-position: -340px -360px; }
+.emoji-1F33B { background-position: -360px -360px; }
+.emoji-1F33C { background-position: -380px 0; }
+.emoji-1F33D { background-position: -380px -20px; }
+.emoji-1F33E { background-position: -380px -40px; }
+.emoji-1F33F { background-position: -380px -60px; }
+.emoji-1F340 { background-position: -380px -80px; }
+.emoji-1F341 { background-position: -380px -100px; }
+.emoji-1F342 { background-position: -380px -120px; }
+.emoji-1F343 { background-position: -380px -140px; }
+.emoji-1F344 { background-position: -380px -160px; }
+.emoji-1F345 { background-position: -380px -180px; }
+.emoji-1F346 { background-position: -380px -200px; }
+.emoji-1F347 { background-position: -380px -220px; }
+.emoji-1F348 { background-position: -380px -240px; }
+.emoji-1F349 { background-position: -380px -260px; }
+.emoji-1F34A { background-position: -380px -280px; }
+.emoji-1F34B { background-position: -380px -300px; }
+.emoji-1F34C { background-position: -380px -320px; }
+.emoji-1F34D { background-position: -380px -340px; }
+.emoji-1F34E { background-position: -380px -360px; }
+.emoji-1F34F { background-position: 0 -380px; }
+.emoji-1F350 { background-position: -20px -380px; }
+.emoji-1F351 { background-position: -40px -380px; }
+.emoji-1F352 { background-position: -60px -380px; }
+.emoji-1F353 { background-position: -80px -380px; }
+.emoji-1F354 { background-position: -100px -380px; }
+.emoji-1F355 { background-position: -120px -380px; }
+.emoji-1F356 { background-position: -140px -380px; }
+.emoji-1F357 { background-position: -160px -380px; }
+.emoji-1F358 { background-position: -180px -380px; }
+.emoji-1F359 { background-position: -200px -380px; }
+.emoji-1F35A { background-position: -220px -380px; }
+.emoji-1F35B { background-position: -240px -380px; }
+.emoji-1F35C { background-position: -260px -380px; }
+.emoji-1F35D { background-position: -280px -380px; }
+.emoji-1F35E { background-position: -300px -380px; }
+.emoji-1F35F { background-position: -320px -380px; }
+.emoji-1F360 { background-position: -340px -380px; }
+.emoji-1F361 { background-position: -360px -380px; }
+.emoji-1F362 { background-position: -380px -380px; }
+.emoji-1F363 { background-position: -400px 0; }
+.emoji-1F364 { background-position: -400px -20px; }
+.emoji-1F365 { background-position: -400px -40px; }
+.emoji-1F366 { background-position: -400px -60px; }
+.emoji-1F367 { background-position: -400px -80px; }
+.emoji-1F368 { background-position: -400px -100px; }
+.emoji-1F369 { background-position: -400px -120px; }
+.emoji-1F36A { background-position: -400px -140px; }
+.emoji-1F36B { background-position: -400px -160px; }
+.emoji-1F36C { background-position: -400px -180px; }
+.emoji-1F36D { background-position: -400px -200px; }
+.emoji-1F36E { background-position: -400px -220px; }
+.emoji-1F36F { background-position: -400px -240px; }
+.emoji-1F370 { background-position: -400px -260px; }
+.emoji-1F371 { background-position: -400px -280px; }
+.emoji-1F372 { background-position: -400px -300px; }
+.emoji-1F373 { background-position: -400px -320px; }
+.emoji-1F374 { background-position: -400px -340px; }
+.emoji-1F375 { background-position: -400px -360px; }
+.emoji-1F376 { background-position: -400px -380px; }
+.emoji-1F377 { background-position: 0 -400px; }
+.emoji-1F378 { background-position: -20px -400px; }
+.emoji-1F379 { background-position: -40px -400px; }
+.emoji-1F37A { background-position: -60px -400px; }
+.emoji-1F37B { background-position: -80px -400px; }
+.emoji-1F37C { background-position: -100px -400px; }
+.emoji-1F37D { background-position: -120px -400px; }
+.emoji-1F37E { background-position: -140px -400px; }
+.emoji-1F37F { background-position: -160px -400px; }
+.emoji-1F380 { background-position: -180px -400px; }
+.emoji-1F381 { background-position: -200px -400px; }
+.emoji-1F382 { background-position: -220px -400px; }
+.emoji-1F383 { background-position: -240px -400px; }
+.emoji-1F384 { background-position: -260px -400px; }
+.emoji-1F385 { background-position: -280px -400px; }
+.emoji-1F385-1F3FB { background-position: -300px -400px; }
+.emoji-1F385-1F3FC { background-position: -320px -400px; }
+.emoji-1F385-1F3FD { background-position: -340px -400px; }
+.emoji-1F385-1F3FE { background-position: -360px -400px; }
+.emoji-1F385-1F3FF { background-position: -380px -400px; }
+.emoji-1F386 { background-position: -400px -400px; }
+.emoji-1F387 { background-position: -420px 0; }
+.emoji-1F388 { background-position: -420px -20px; }
+.emoji-1F389 { background-position: -420px -40px; }
+.emoji-1F38A { background-position: -420px -60px; }
+.emoji-1F38B { background-position: -420px -80px; }
+.emoji-1F38C { background-position: -420px -100px; }
+.emoji-1F38D { background-position: -420px -120px; }
+.emoji-1F38E { background-position: -420px -140px; }
+.emoji-1F38F { background-position: -420px -160px; }
+.emoji-1F390 { background-position: -420px -180px; }
+.emoji-1F391 { background-position: -420px -200px; }
+.emoji-1F392 { background-position: -420px -220px; }
+.emoji-1F393 { background-position: -420px -240px; }
+.emoji-1F396 { background-position: -420px -260px; }
+.emoji-1F397 { background-position: -420px -280px; }
+.emoji-1F399 { background-position: -420px -300px; }
+.emoji-1F39A { background-position: -420px -320px; }
+.emoji-1F39B { background-position: -420px -340px; }
+.emoji-1F39E { background-position: -420px -360px; }
+.emoji-1F39F { background-position: -420px -380px; }
+.emoji-1F3A0 { background-position: -420px -400px; }
+.emoji-1F3A1 { background-position: 0 -420px; }
+.emoji-1F3A2 { background-position: -20px -420px; }
+.emoji-1F3A3 { background-position: -40px -420px; }
+.emoji-1F3A4 { background-position: -60px -420px; }
+.emoji-1F3A5 { background-position: -80px -420px; }
+.emoji-1F3A6 { background-position: -100px -420px; }
+.emoji-1F3A7 { background-position: -120px -420px; }
+.emoji-1F3A8 { background-position: -140px -420px; }
+.emoji-1F3A9 { background-position: -160px -420px; }
+.emoji-1F3AA { background-position: -180px -420px; }
+.emoji-1F3AB { background-position: -200px -420px; }
+.emoji-1F3AC { background-position: -220px -420px; }
+.emoji-1F3AD { background-position: -240px -420px; }
+.emoji-1F3AE { background-position: -260px -420px; }
+.emoji-1F3AF { background-position: -280px -420px; }
+.emoji-1F3B0 { background-position: -300px -420px; }
+.emoji-1F3B1 { background-position: -320px -420px; }
+.emoji-1F3B2 { background-position: -340px -420px; }
+.emoji-1F3B3 { background-position: -360px -420px; }
+.emoji-1F3B4 { background-position: -380px -420px; }
+.emoji-1F3B5 { background-position: -400px -420px; }
+.emoji-1F3B6 { background-position: -420px -420px; }
+.emoji-1F3B7 { background-position: -440px 0; }
+.emoji-1F3B8 { background-position: -440px -20px; }
+.emoji-1F3B9 { background-position: -440px -40px; }
+.emoji-1F3BA { background-position: -440px -60px; }
+.emoji-1F3BB { background-position: -440px -80px; }
+.emoji-1F3BC { background-position: -440px -100px; }
+.emoji-1F3BD { background-position: -440px -120px; }
+.emoji-1F3BE { background-position: -440px -140px; }
+.emoji-1F3BF { background-position: -440px -160px; }
+.emoji-1F3C0 { background-position: -440px -180px; }
+.emoji-1F3C1 { background-position: -440px -200px; }
+.emoji-1F3C2 { background-position: -440px -220px; }
+.emoji-1F3C3 { background-position: -440px -240px; }
+.emoji-1F3C3-1F3FB { background-position: -440px -260px; }
+.emoji-1F3C3-1F3FC { background-position: -440px -280px; }
+.emoji-1F3C3-1F3FD { background-position: -440px -300px; }
+.emoji-1F3C3-1F3FE { background-position: -440px -320px; }
+.emoji-1F3C3-1F3FF { background-position: -440px -340px; }
+.emoji-1F3C4 { background-position: -440px -360px; }
+.emoji-1F3C4-1F3FB { background-position: -440px -380px; }
+.emoji-1F3C4-1F3FC { background-position: -440px -400px; }
+.emoji-1F3C4-1F3FD { background-position: -440px -420px; }
+.emoji-1F3C4-1F3FE { background-position: 0 -440px; }
+.emoji-1F3C4-1F3FF { background-position: -20px -440px; }
+.emoji-1F3C5 { background-position: -40px -440px; }
+.emoji-1F3C6 { background-position: -60px -440px; }
+.emoji-1F3C7 { background-position: -80px -440px; }
+.emoji-1F3C7-1F3FB { background-position: -100px -440px; }
+.emoji-1F3C7-1F3FC { background-position: -120px -440px; }
+.emoji-1F3C7-1F3FD { background-position: -140px -440px; }
+.emoji-1F3C7-1F3FE { background-position: -160px -440px; }
+.emoji-1F3C7-1F3FF { background-position: -180px -440px; }
+.emoji-1F3C8 { background-position: -200px -440px; }
+.emoji-1F3C9 { background-position: -220px -440px; }
+.emoji-1F3CA { background-position: -240px -440px; }
+.emoji-1F3CA-1F3FB { background-position: -260px -440px; }
+.emoji-1F3CA-1F3FC { background-position: -280px -440px; }
+.emoji-1F3CA-1F3FD { background-position: -300px -440px; }
+.emoji-1F3CA-1F3FE { background-position: -320px -440px; }
+.emoji-1F3CA-1F3FF { background-position: -340px -440px; }
+.emoji-1F3CB { background-position: -360px -440px; }
+.emoji-1F3CB-1F3FB { background-position: -380px -440px; }
+.emoji-1F3CB-1F3FC { background-position: -400px -440px; }
+.emoji-1F3CB-1F3FD { background-position: -420px -440px; }
+.emoji-1F3CB-1F3FE { background-position: -440px -440px; }
+.emoji-1F3CB-1F3FF { background-position: -460px 0; }
+.emoji-1F3CC { background-position: -460px -20px; }
+.emoji-1F3CD { background-position: -460px -40px; }
+.emoji-1F3CE { background-position: -460px -60px; }
+.emoji-1F3CF { background-position: -460px -80px; }
+.emoji-1F3D0 { background-position: -460px -100px; }
+.emoji-1F3D1 { background-position: -460px -120px; }
+.emoji-1F3D2 { background-position: -460px -140px; }
+.emoji-1F3D3 { background-position: -460px -160px; }
+.emoji-1F3D4 { background-position: -460px -180px; }
+.emoji-1F3D5 { background-position: -460px -200px; }
+.emoji-1F3D6 { background-position: -460px -220px; }
+.emoji-1F3D7 { background-position: -460px -240px; }
+.emoji-1F3D8 { background-position: -460px -260px; }
+.emoji-1F3D9 { background-position: -460px -280px; }
+.emoji-1F3DA { background-position: -460px -300px; }
+.emoji-1F3DB { background-position: -460px -320px; }
+.emoji-1F3DC { background-position: -460px -340px; }
+.emoji-1F3DD { background-position: -460px -360px; }
+.emoji-1F3DE { background-position: -460px -380px; }
+.emoji-1F3DF { background-position: -460px -400px; }
+.emoji-1F3E0 { background-position: -460px -420px; }
+.emoji-1F3E1 { background-position: -460px -440px; }
+.emoji-1F3E2 { background-position: 0 -460px; }
+.emoji-1F3E3 { background-position: -20px -460px; }
+.emoji-1F3E4 { background-position: -40px -460px; }
+.emoji-1F3E5 { background-position: -60px -460px; }
+.emoji-1F3E6 { background-position: -80px -460px; }
+.emoji-1F3E7 { background-position: -100px -460px; }
+.emoji-1F3E8 { background-position: -120px -460px; }
+.emoji-1F3E9 { background-position: -140px -460px; }
+.emoji-1F3EA { background-position: -160px -460px; }
+.emoji-1F3EB { background-position: -180px -460px; }
+.emoji-1F3EC { background-position: -200px -460px; }
+.emoji-1F3ED { background-position: -220px -460px; }
+.emoji-1F3EE { background-position: -240px -460px; }
+.emoji-1F3EF { background-position: -260px -460px; }
+.emoji-1F3F0 { background-position: -280px -460px; }
+.emoji-1F3F3 { background-position: -300px -460px; }
+.emoji-1F3F4 { background-position: -320px -460px; }
+.emoji-1F3F5 { background-position: -340px -460px; }
+.emoji-1F3F7 { background-position: -360px -460px; }
+.emoji-1F3F8 { background-position: -380px -460px; }
+.emoji-1F3F9 { background-position: -400px -460px; }
+.emoji-1F3FA { background-position: -420px -460px; }
+.emoji-1F3FB { background-position: -440px -460px; }
+.emoji-1F3FC { background-position: -460px -460px; }
+.emoji-1F3FD { background-position: -480px 0; }
+.emoji-1F3FE { background-position: -480px -20px; }
+.emoji-1F3FF { background-position: -480px -40px; }
+.emoji-1F400 { background-position: -480px -60px; }
+.emoji-1F401 { background-position: -480px -80px; }
+.emoji-1F402 { background-position: -480px -100px; }
+.emoji-1F403 { background-position: -480px -120px; }
+.emoji-1F404 { background-position: -480px -140px; }
+.emoji-1F405 { background-position: -480px -160px; }
+.emoji-1F406 { background-position: -480px -180px; }
+.emoji-1F407 { background-position: -480px -200px; }
+.emoji-1F408 { background-position: -480px -220px; }
+.emoji-1F409 { background-position: -480px -240px; }
+.emoji-1F40A { background-position: -480px -260px; }
+.emoji-1F40B { background-position: -480px -280px; }
+.emoji-1F40C { background-position: -480px -300px; }
+.emoji-1F40D { background-position: -480px -320px; }
+.emoji-1F40E { background-position: -480px -340px; }
+.emoji-1F40F { background-position: -480px -360px; }
+.emoji-1F410 { background-position: -480px -380px; }
+.emoji-1F411 { background-position: -480px -400px; }
+.emoji-1F412 { background-position: -480px -420px; }
+.emoji-1F413 { background-position: -480px -440px; }
+.emoji-1F414 { background-position: -480px -460px; }
+.emoji-1F415 { background-position: 0 -480px; }
+.emoji-1F416 { background-position: -20px -480px; }
+.emoji-1F417 { background-position: -40px -480px; }
+.emoji-1F418 { background-position: -60px -480px; }
+.emoji-1F419 { background-position: -80px -480px; }
+.emoji-1F41A { background-position: -100px -480px; }
+.emoji-1F41B { background-position: -120px -480px; }
+.emoji-1F41C { background-position: -140px -480px; }
+.emoji-1F41D { background-position: -160px -480px; }
+.emoji-1F41E { background-position: -180px -480px; }
+.emoji-1F41F { background-position: -200px -480px; }
+.emoji-1F420 { background-position: -220px -480px; }
+.emoji-1F421 { background-position: -240px -480px; }
+.emoji-1F422 { background-position: -260px -480px; }
+.emoji-1F423 { background-position: -280px -480px; }
+.emoji-1F424 { background-position: -300px -480px; }
+.emoji-1F425 { background-position: -320px -480px; }
+.emoji-1F426 { background-position: -340px -480px; }
+.emoji-1F427 { background-position: -360px -480px; }
+.emoji-1F428 { background-position: -380px -480px; }
+.emoji-1F429 { background-position: -400px -480px; }
+.emoji-1F42A { background-position: -420px -480px; }
+.emoji-1F42B { background-position: -440px -480px; }
+.emoji-1F42C { background-position: -460px -480px; }
+.emoji-1F42D { background-position: -480px -480px; }
+.emoji-1F42E { background-position: -500px 0; }
+.emoji-1F42F { background-position: -500px -20px; }
+.emoji-1F430 { background-position: -500px -40px; }
+.emoji-1F431 { background-position: -500px -60px; }
+.emoji-1F432 { background-position: -500px -80px; }
+.emoji-1F433 { background-position: -500px -100px; }
+.emoji-1F434 { background-position: -500px -120px; }
+.emoji-1F435 { background-position: -500px -140px; }
+.emoji-1F436 { background-position: -500px -160px; }
+.emoji-1F437 { background-position: -500px -180px; }
+.emoji-1F438 { background-position: -500px -200px; }
+.emoji-1F439 { background-position: -500px -220px; }
+.emoji-1F43A { background-position: -500px -240px; }
+.emoji-1F43B { background-position: -500px -260px; }
+.emoji-1F43C { background-position: -500px -280px; }
+.emoji-1F43D { background-position: -500px -300px; }
+.emoji-1F43E { background-position: -500px -320px; }
+.emoji-1F43F { background-position: -500px -340px; }
+.emoji-1F440 { background-position: -500px -360px; }
+.emoji-1F441 { background-position: -500px -380px; }
+.emoji-1F441-1F5E8 { background-position: -500px -400px; }
+.emoji-1F442 { background-position: -500px -420px; }
+.emoji-1F442-1F3FB { background-position: -500px -440px; }
+.emoji-1F442-1F3FC { background-position: -500px -460px; }
+.emoji-1F442-1F3FD { background-position: -500px -480px; }
+.emoji-1F442-1F3FE { background-position: 0 -500px; }
+.emoji-1F442-1F3FF { background-position: -20px -500px; }
+.emoji-1F443 { background-position: -40px -500px; }
+.emoji-1F443-1F3FB { background-position: -60px -500px; }
+.emoji-1F443-1F3FC { background-position: -80px -500px; }
+.emoji-1F443-1F3FD { background-position: -100px -500px; }
+.emoji-1F443-1F3FE { background-position: -120px -500px; }
+.emoji-1F443-1F3FF { background-position: -140px -500px; }
+.emoji-1F444 { background-position: -160px -500px; }
+.emoji-1F445 { background-position: -180px -500px; }
+.emoji-1F446 { background-position: -200px -500px; }
+.emoji-1F446-1F3FB { background-position: -220px -500px; }
+.emoji-1F446-1F3FC { background-position: -240px -500px; }
+.emoji-1F446-1F3FD { background-position: -260px -500px; }
+.emoji-1F446-1F3FE { background-position: -280px -500px; }
+.emoji-1F446-1F3FF { background-position: -300px -500px; }
+.emoji-1F447 { background-position: -320px -500px; }
+.emoji-1F447-1F3FB { background-position: -340px -500px; }
+.emoji-1F447-1F3FC { background-position: -360px -500px; }
+.emoji-1F447-1F3FD { background-position: -380px -500px; }
+.emoji-1F447-1F3FE { background-position: -400px -500px; }
+.emoji-1F447-1F3FF { background-position: -420px -500px; }
+.emoji-1F448 { background-position: -440px -500px; }
+.emoji-1F448-1F3FB { background-position: -460px -500px; }
+.emoji-1F448-1F3FC { background-position: -480px -500px; }
+.emoji-1F448-1F3FD { background-position: -500px -500px; }
+.emoji-1F448-1F3FE { background-position: -520px 0; }
+.emoji-1F448-1F3FF { background-position: -520px -20px; }
+.emoji-1F449 { background-position: -520px -40px; }
+.emoji-1F449-1F3FB { background-position: -520px -60px; }
+.emoji-1F449-1F3FC { background-position: -520px -80px; }
+.emoji-1F449-1F3FD { background-position: -520px -100px; }
+.emoji-1F449-1F3FE { background-position: -520px -120px; }
+.emoji-1F449-1F3FF { background-position: -520px -140px; }
+.emoji-1F44A { background-position: -520px -160px; }
+.emoji-1F44A-1F3FB { background-position: -520px -180px; }
+.emoji-1F44A-1F3FC { background-position: -520px -200px; }
+.emoji-1F44A-1F3FD { background-position: -520px -220px; }
+.emoji-1F44A-1F3FE { background-position: -520px -240px; }
+.emoji-1F44A-1F3FF { background-position: -520px -260px; }
+.emoji-1F44B { background-position: -520px -280px; }
+.emoji-1F44B-1F3FB { background-position: -520px -300px; }
+.emoji-1F44B-1F3FC { background-position: -520px -320px; }
+.emoji-1F44B-1F3FD { background-position: -520px -340px; }
+.emoji-1F44B-1F3FE { background-position: -520px -360px; }
+.emoji-1F44B-1F3FF { background-position: -520px -380px; }
+.emoji-1F44C { background-position: -520px -400px; }
+.emoji-1F44C-1F3FB { background-position: -520px -420px; }
+.emoji-1F44C-1F3FC { background-position: -520px -440px; }
+.emoji-1F44C-1F3FD { background-position: -520px -460px; }
+.emoji-1F44C-1F3FE { background-position: -520px -480px; }
+.emoji-1F44C-1F3FF { background-position: -520px -500px; }
+.emoji-1F44D { background-position: 0 -520px; }
+.emoji-1F44D-1F3FB { background-position: -20px -520px; }
+.emoji-1F44D-1F3FC { background-position: -40px -520px; }
+.emoji-1F44D-1F3FD { background-position: -60px -520px; }
+.emoji-1F44D-1F3FE { background-position: -80px -520px; }
+.emoji-1F44D-1F3FF { background-position: -100px -520px; }
+.emoji-1F44E { background-position: -120px -520px; }
+.emoji-1F44E-1F3FB { background-position: -140px -520px; }
+.emoji-1F44E-1F3FC { background-position: -160px -520px; }
+.emoji-1F44E-1F3FD { background-position: -180px -520px; }
+.emoji-1F44E-1F3FE { background-position: -200px -520px; }
+.emoji-1F44E-1F3FF { background-position: -220px -520px; }
+.emoji-1F44F { background-position: -240px -520px; }
+.emoji-1F44F-1F3FB { background-position: -260px -520px; }
+.emoji-1F44F-1F3FC { background-position: -280px -520px; }
+.emoji-1F44F-1F3FD { background-position: -300px -520px; }
+.emoji-1F44F-1F3FE { background-position: -320px -520px; }
+.emoji-1F44F-1F3FF { background-position: -340px -520px; }
+.emoji-1F450 { background-position: -360px -520px; }
+.emoji-1F450-1F3FB { background-position: -380px -520px; }
+.emoji-1F450-1F3FC { background-position: -400px -520px; }
+.emoji-1F450-1F3FD { background-position: -420px -520px; }
+.emoji-1F450-1F3FE { background-position: -440px -520px; }
+.emoji-1F450-1F3FF { background-position: -460px -520px; }
+.emoji-1F451 { background-position: -480px -520px; }
+.emoji-1F452 { background-position: -500px -520px; }
+.emoji-1F453 { background-position: -520px -520px; }
+.emoji-1F454 { background-position: -540px 0; }
+.emoji-1F455 { background-position: -540px -20px; }
+.emoji-1F456 { background-position: -540px -40px; }
+.emoji-1F457 { background-position: -540px -60px; }
+.emoji-1F458 { background-position: -540px -80px; }
+.emoji-1F459 { background-position: -540px -100px; }
+.emoji-1F45A { background-position: -540px -120px; }
+.emoji-1F45B { background-position: -540px -140px; }
+.emoji-1F45C { background-position: -540px -160px; }
+.emoji-1F45D { background-position: -540px -180px; }
+.emoji-1F45E { background-position: -540px -200px; }
+.emoji-1F45F { background-position: -540px -220px; }
+.emoji-1F460 { background-position: -540px -240px; }
+.emoji-1F461 { background-position: -540px -260px; }
+.emoji-1F462 { background-position: -540px -280px; }
+.emoji-1F463 { background-position: -540px -300px; }
+.emoji-1F464 { background-position: -540px -320px; }
+.emoji-1F465 { background-position: -540px -340px; }
+.emoji-1F466 { background-position: -540px -360px; }
+.emoji-1F466-1F3FB { background-position: -540px -380px; }
+.emoji-1F466-1F3FC { background-position: -540px -400px; }
+.emoji-1F466-1F3FD { background-position: -540px -420px; }
+.emoji-1F466-1F3FE { background-position: -540px -440px; }
+.emoji-1F466-1F3FF { background-position: -540px -460px; }
+.emoji-1F467 { background-position: -540px -480px; }
+.emoji-1F467-1F3FB { background-position: -540px -500px; }
+.emoji-1F467-1F3FC { background-position: -540px -520px; }
+.emoji-1F467-1F3FD { background-position: 0 -540px; }
+.emoji-1F467-1F3FE { background-position: -20px -540px; }
+.emoji-1F467-1F3FF { background-position: -40px -540px; }
+.emoji-1F468 { background-position: -60px -540px; }
+.emoji-1F468-1F3FB { background-position: -80px -540px; }
+.emoji-1F468-1F3FC { background-position: -100px -540px; }
+.emoji-1F468-1F3FD { background-position: -120px -540px; }
+.emoji-1F468-1F3FE { background-position: -140px -540px; }
+.emoji-1F468-1F3FF { background-position: -160px -540px; }
+.emoji-1F468-1F468-1F466 { background-position: -180px -540px; }
+.emoji-1F468-1F468-1F466-1F466 { background-position: -200px -540px; }
+.emoji-1F468-1F468-1F467 { background-position: -220px -540px; }
+.emoji-1F468-1F468-1F467-1F466 { background-position: -240px -540px; }
+.emoji-1F468-1F468-1F467-1F467 { background-position: -260px -540px; }
+.emoji-1F468-1F469-1F466-1F466 { background-position: -280px -540px; }
+.emoji-1F468-1F469-1F467 { background-position: -300px -540px; }
+.emoji-1F468-1F469-1F467-1F466 { background-position: -320px -540px; }
+.emoji-1F468-1F469-1F467-1F467 { background-position: -340px -540px; }
+.emoji-1F468-2764-1F468 { background-position: -360px -540px; }
+.emoji-1F468-2764-1F48B-1F468 { background-position: -380px -540px; }
+.emoji-1F469 { background-position: -400px -540px; }
+.emoji-1F469-1F3FB { background-position: -420px -540px; }
+.emoji-1F469-1F3FC { background-position: -440px -540px; }
+.emoji-1F469-1F3FD { background-position: -460px -540px; }
+.emoji-1F469-1F3FE { background-position: -480px -540px; }
+.emoji-1F469-1F3FF { background-position: -500px -540px; }
+.emoji-1F469-1F469-1F466 { background-position: -520px -540px; }
+.emoji-1F469-1F469-1F466-1F466 { background-position: -540px -540px; }
+.emoji-1F469-1F469-1F467 { background-position: -560px 0; }
+.emoji-1F469-1F469-1F467-1F466 { background-position: -560px -20px; }
+.emoji-1F469-1F469-1F467-1F467 { background-position: -560px -40px; }
+.emoji-1F469-2764-1F469 { background-position: -560px -60px; }
+.emoji-1F469-2764-1F48B-1F469 { background-position: -560px -80px; }
+.emoji-1F46A { background-position: -560px -100px; }
+.emoji-1F46B { background-position: -560px -120px; }
+.emoji-1F46C { background-position: -560px -140px; }
+.emoji-1F46D { background-position: -560px -160px; }
+.emoji-1F46E { background-position: -560px -180px; }
+.emoji-1F46E-1F3FB { background-position: -560px -200px; }
+.emoji-1F46E-1F3FC { background-position: -560px -220px; }
+.emoji-1F46E-1F3FD { background-position: -560px -240px; }
+.emoji-1F46E-1F3FE { background-position: -560px -260px; }
+.emoji-1F46E-1F3FF { background-position: -560px -280px; }
+.emoji-1F46F { background-position: -560px -300px; }
+.emoji-1F470 { background-position: -560px -320px; }
+.emoji-1F470-1F3FB { background-position: -560px -340px; }
+.emoji-1F470-1F3FC { background-position: -560px -360px; }
+.emoji-1F470-1F3FD { background-position: -560px -380px; }
+.emoji-1F470-1F3FE { background-position: -560px -400px; }
+.emoji-1F470-1F3FF { background-position: -560px -420px; }
+.emoji-1F471 { background-position: -560px -440px; }
+.emoji-1F471-1F3FB { background-position: -560px -460px; }
+.emoji-1F471-1F3FC { background-position: -560px -480px; }
+.emoji-1F471-1F3FD { background-position: -560px -500px; }
+.emoji-1F471-1F3FE { background-position: -560px -520px; }
+.emoji-1F471-1F3FF { background-position: -560px -540px; }
+.emoji-1F472 { background-position: 0 -560px; }
+.emoji-1F472-1F3FB { background-position: -20px -560px; }
+.emoji-1F472-1F3FC { background-position: -40px -560px; }
+.emoji-1F472-1F3FD { background-position: -60px -560px; }
+.emoji-1F472-1F3FE { background-position: -80px -560px; }
+.emoji-1F472-1F3FF { background-position: -100px -560px; }
+.emoji-1F473 { background-position: -120px -560px; }
+.emoji-1F473-1F3FB { background-position: -140px -560px; }
+.emoji-1F473-1F3FC { background-position: -160px -560px; }
+.emoji-1F473-1F3FD { background-position: -180px -560px; }
+.emoji-1F473-1F3FE { background-position: -200px -560px; }
+.emoji-1F473-1F3FF { background-position: -220px -560px; }
+.emoji-1F474 { background-position: -240px -560px; }
+.emoji-1F474-1F3FB { background-position: -260px -560px; }
+.emoji-1F474-1F3FC { background-position: -280px -560px; }
+.emoji-1F474-1F3FD { background-position: -300px -560px; }
+.emoji-1F474-1F3FE { background-position: -320px -560px; }
+.emoji-1F474-1F3FF { background-position: -340px -560px; }
+.emoji-1F475 { background-position: -360px -560px; }
+.emoji-1F475-1F3FB { background-position: -380px -560px; }
+.emoji-1F475-1F3FC { background-position: -400px -560px; }
+.emoji-1F475-1F3FD { background-position: -420px -560px; }
+.emoji-1F475-1F3FE { background-position: -440px -560px; }
+.emoji-1F475-1F3FF { background-position: -460px -560px; }
+.emoji-1F476 { background-position: -480px -560px; }
+.emoji-1F476-1F3FB { background-position: -500px -560px; }
+.emoji-1F476-1F3FC { background-position: -520px -560px; }
+.emoji-1F476-1F3FD { background-position: -540px -560px; }
+.emoji-1F476-1F3FE { background-position: -560px -560px; }
+.emoji-1F476-1F3FF { background-position: -580px 0; }
+.emoji-1F477 { background-position: -580px -20px; }
+.emoji-1F477-1F3FB { background-position: -580px -40px; }
+.emoji-1F477-1F3FC { background-position: -580px -60px; }
+.emoji-1F477-1F3FD { background-position: -580px -80px; }
+.emoji-1F477-1F3FE { background-position: -580px -100px; }
+.emoji-1F477-1F3FF { background-position: -580px -120px; }
+.emoji-1F478 { background-position: -580px -140px; }
+.emoji-1F478-1F3FB { background-position: -580px -160px; }
+.emoji-1F478-1F3FC { background-position: -580px -180px; }
+.emoji-1F478-1F3FD { background-position: -580px -200px; }
+.emoji-1F478-1F3FE { background-position: -580px -220px; }
+.emoji-1F478-1F3FF { background-position: -580px -240px; }
+.emoji-1F479 { background-position: -580px -260px; }
+.emoji-1F47A { background-position: -580px -280px; }
+.emoji-1F47B { background-position: -580px -300px; }
+.emoji-1F47C { background-position: -580px -320px; }
+.emoji-1F47C-1F3FB { background-position: -580px -340px; }
+.emoji-1F47C-1F3FC { background-position: -580px -360px; }
+.emoji-1F47C-1F3FD { background-position: -580px -380px; }
+.emoji-1F47C-1F3FE { background-position: -580px -400px; }
+.emoji-1F47C-1F3FF { background-position: -580px -420px; }
+.emoji-1F47D { background-position: -580px -440px; }
+.emoji-1F47E { background-position: -580px -460px; }
+.emoji-1F47F { background-position: -580px -480px; }
+.emoji-1F480 { background-position: -580px -500px; }
+.emoji-1F481 { background-position: -580px -520px; }
+.emoji-1F481-1F3FB { background-position: -580px -540px; }
+.emoji-1F481-1F3FC { background-position: -580px -560px; }
+.emoji-1F481-1F3FD { background-position: 0 -580px; }
+.emoji-1F481-1F3FE { background-position: -20px -580px; }
+.emoji-1F481-1F3FF { background-position: -40px -580px; }
+.emoji-1F482 { background-position: -60px -580px; }
+.emoji-1F482-1F3FB { background-position: -80px -580px; }
+.emoji-1F482-1F3FC { background-position: -100px -580px; }
+.emoji-1F482-1F3FD { background-position: -120px -580px; }
+.emoji-1F482-1F3FE { background-position: -140px -580px; }
+.emoji-1F482-1F3FF { background-position: -160px -580px; }
+.emoji-1F483 { background-position: -180px -580px; }
+.emoji-1F483-1F3FB { background-position: -200px -580px; }
+.emoji-1F483-1F3FC { background-position: -220px -580px; }
+.emoji-1F483-1F3FD { background-position: -240px -580px; }
+.emoji-1F483-1F3FE { background-position: -260px -580px; }
+.emoji-1F483-1F3FF { background-position: -280px -580px; }
+.emoji-1F484 { background-position: -300px -580px; }
+.emoji-1F485 { background-position: -320px -580px; }
+.emoji-1F485-1F3FB { background-position: -340px -580px; }
+.emoji-1F485-1F3FC { background-position: -360px -580px; }
+.emoji-1F485-1F3FD { background-position: -380px -580px; }
+.emoji-1F485-1F3FE { background-position: -400px -580px; }
+.emoji-1F485-1F3FF { background-position: -420px -580px; }
+.emoji-1F486 { background-position: -440px -580px; }
+.emoji-1F486-1F3FB { background-position: -460px -580px; }
+.emoji-1F486-1F3FC { background-position: -480px -580px; }
+.emoji-1F486-1F3FD { background-position: -500px -580px; }
+.emoji-1F486-1F3FE { background-position: -520px -580px; }
+.emoji-1F486-1F3FF { background-position: -540px -580px; }
+.emoji-1F487 { background-position: -560px -580px; }
+.emoji-1F487-1F3FB { background-position: -580px -580px; }
+.emoji-1F487-1F3FC { background-position: -600px 0; }
+.emoji-1F487-1F3FD { background-position: -600px -20px; }
+.emoji-1F487-1F3FE { background-position: -600px -40px; }
+.emoji-1F487-1F3FF { background-position: -600px -60px; }
+.emoji-1F488 { background-position: -600px -80px; }
+.emoji-1F489 { background-position: -600px -100px; }
+.emoji-1F48A { background-position: -600px -120px; }
+.emoji-1F48B { background-position: -600px -140px; }
+.emoji-1F48C { background-position: -600px -160px; }
+.emoji-1F48D { background-position: -600px -180px; }
+.emoji-1F48E { background-position: -600px -200px; }
+.emoji-1F48F { background-position: -600px -220px; }
+.emoji-1F490 { background-position: -600px -240px; }
+.emoji-1F491 { background-position: -600px -260px; }
+.emoji-1F492 { background-position: -600px -280px; }
+.emoji-1F493 { background-position: -600px -300px; }
+.emoji-1F494 { background-position: -600px -320px; }
+.emoji-1F495 { background-position: -600px -340px; }
+.emoji-1F496 { background-position: -600px -360px; }
+.emoji-1F497 { background-position: -600px -380px; }
+.emoji-1F498 { background-position: -600px -400px; }
+.emoji-1F499 { background-position: -600px -420px; }
+.emoji-1F49A { background-position: -600px -440px; }
+.emoji-1F49B { background-position: -600px -460px; }
+.emoji-1F49C { background-position: -600px -480px; }
+.emoji-1F49D { background-position: -600px -500px; }
+.emoji-1F49E { background-position: -600px -520px; }
+.emoji-1F49F { background-position: -600px -540px; }
+.emoji-1F4A0 { background-position: -600px -560px; }
+.emoji-1F4A1 { background-position: -600px -580px; }
+.emoji-1F4A2 { background-position: 0 -600px; }
+.emoji-1F4A3 { background-position: -20px -600px; }
+.emoji-1F4A4 { background-position: -40px -600px; }
+.emoji-1F4A5 { background-position: -60px -600px; }
+.emoji-1F4A6 { background-position: -80px -600px; }
+.emoji-1F4A7 { background-position: -100px -600px; }
+.emoji-1F4A8 { background-position: -120px -600px; }
+.emoji-1F4A9 { background-position: -140px -600px; }
+.emoji-1F4AA { background-position: -160px -600px; }
+.emoji-1F4AA-1F3FB { background-position: -180px -600px; }
+.emoji-1F4AA-1F3FC { background-position: -200px -600px; }
+.emoji-1F4AA-1F3FD { background-position: -220px -600px; }
+.emoji-1F4AA-1F3FE { background-position: -240px -600px; }
+.emoji-1F4AA-1F3FF { background-position: -260px -600px; }
+.emoji-1F4AB { background-position: -280px -600px; }
+.emoji-1F4AC { background-position: -300px -600px; }
+.emoji-1F4AD { background-position: -320px -600px; }
+.emoji-1F4AE { background-position: -340px -600px; }
+.emoji-1F4AF { background-position: -360px -600px; }
+.emoji-1F4B0 { background-position: -380px -600px; }
+.emoji-1F4B1 { background-position: -400px -600px; }
+.emoji-1F4B2 { background-position: -420px -600px; }
+.emoji-1F4B3 { background-position: -440px -600px; }
+.emoji-1F4B4 { background-position: -460px -600px; }
+.emoji-1F4B5 { background-position: -480px -600px; }
+.emoji-1F4B6 { background-position: -500px -600px; }
+.emoji-1F4B7 { background-position: -520px -600px; }
+.emoji-1F4B8 { background-position: -540px -600px; }
+.emoji-1F4B9 { background-position: -560px -600px; }
+.emoji-1F4BA { background-position: -580px -600px; }
+.emoji-1F4BB { background-position: -600px -600px; }
+.emoji-1F4BC { background-position: -620px 0; }
+.emoji-1F4BD { background-position: -620px -20px; }
+.emoji-1F4BE { background-position: -620px -40px; }
+.emoji-1F4BF { background-position: -620px -60px; }
+.emoji-1F4C0 { background-position: -620px -80px; }
+.emoji-1F4C1 { background-position: -620px -100px; }
+.emoji-1F4C2 { background-position: -620px -120px; }
+.emoji-1F4C3 { background-position: -620px -140px; }
+.emoji-1F4C4 { background-position: -620px -160px; }
+.emoji-1F4C5 { background-position: -620px -180px; }
+.emoji-1F4C6 { background-position: -620px -200px; }
+.emoji-1F4C7 { background-position: -620px -220px; }
+.emoji-1F4C8 { background-position: -620px -240px; }
+.emoji-1F4C9 { background-position: -620px -260px; }
+.emoji-1F4CA { background-position: -620px -280px; }
+.emoji-1F4CB { background-position: -620px -300px; }
+.emoji-1F4CC { background-position: -620px -320px; }
+.emoji-1F4CD { background-position: -620px -340px; }
+.emoji-1F4CE { background-position: -620px -360px; }
+.emoji-1F4CF { background-position: -620px -380px; }
+.emoji-1F4D0 { background-position: -620px -400px; }
+.emoji-1F4D1 { background-position: -620px -420px; }
+.emoji-1F4D2 { background-position: -620px -440px; }
+.emoji-1F4D3 { background-position: -620px -460px; }
+.emoji-1F4D4 { background-position: -620px -480px; }
+.emoji-1F4D5 { background-position: -620px -500px; }
+.emoji-1F4D6 { background-position: -620px -520px; }
+.emoji-1F4D7 { background-position: -620px -540px; }
+.emoji-1F4D8 { background-position: -620px -560px; }
+.emoji-1F4D9 { background-position: -620px -580px; }
+.emoji-1F4DA { background-position: -620px -600px; }
+.emoji-1F4DB { background-position: 0 -620px; }
+.emoji-1F4DC { background-position: -20px -620px; }
+.emoji-1F4DD { background-position: -40px -620px; }
+.emoji-1F4DE { background-position: -60px -620px; }
+.emoji-1F4DF { background-position: -80px -620px; }
+.emoji-1F4E0 { background-position: -100px -620px; }
+.emoji-1F4E1 { background-position: -120px -620px; }
+.emoji-1F4E2 { background-position: -140px -620px; }
+.emoji-1F4E3 { background-position: -160px -620px; }
+.emoji-1F4E4 { background-position: -180px -620px; }
+.emoji-1F4E5 { background-position: -200px -620px; }
+.emoji-1F4E6 { background-position: -220px -620px; }
+.emoji-1F4E7 { background-position: -240px -620px; }
+.emoji-1F4E8 { background-position: -260px -620px; }
+.emoji-1F4E9 { background-position: -280px -620px; }
+.emoji-1F4EA { background-position: -300px -620px; }
+.emoji-1F4EB { background-position: -320px -620px; }
+.emoji-1F4EC { background-position: -340px -620px; }
+.emoji-1F4ED { background-position: -360px -620px; }
+.emoji-1F4EE { background-position: -380px -620px; }
+.emoji-1F4EF { background-position: -400px -620px; }
+.emoji-1F4F0 { background-position: -420px -620px; }
+.emoji-1F4F1 { background-position: -440px -620px; }
+.emoji-1F4F2 { background-position: -460px -620px; }
+.emoji-1F4F3 { background-position: -480px -620px; }
+.emoji-1F4F4 { background-position: -500px -620px; }
+.emoji-1F4F5 { background-position: -520px -620px; }
+.emoji-1F4F6 { background-position: -540px -620px; }
+.emoji-1F4F7 { background-position: -560px -620px; }
+.emoji-1F4F8 { background-position: -580px -620px; }
+.emoji-1F4F9 { background-position: -600px -620px; }
+.emoji-1F4FA { background-position: -620px -620px; }
+.emoji-1F4FB { background-position: -640px 0; }
+.emoji-1F4FC { background-position: -640px -20px; }
+.emoji-1F4FD { background-position: -640px -40px; }
+.emoji-1F4FF { background-position: -640px -60px; }
+.emoji-1F500 { background-position: -640px -80px; }
+.emoji-1F501 { background-position: -640px -100px; }
+.emoji-1F502 { background-position: -640px -120px; }
+.emoji-1F503 { background-position: -640px -140px; }
+.emoji-1F504 { background-position: -640px -160px; }
+.emoji-1F505 { background-position: -640px -180px; }
+.emoji-1F506 { background-position: -640px -200px; }
+.emoji-1F507 { background-position: -640px -220px; }
+.emoji-1F508 { background-position: -640px -240px; }
+.emoji-1F509 { background-position: -640px -260px; }
+.emoji-1F50A { background-position: -640px -280px; }
+.emoji-1F50B { background-position: -640px -300px; }
+.emoji-1F50C { background-position: -640px -320px; }
+.emoji-1F50D { background-position: -640px -340px; }
+.emoji-1F50E { background-position: -640px -360px; }
+.emoji-1F50F { background-position: -640px -380px; }
+.emoji-1F510 { background-position: -640px -400px; }
+.emoji-1F511 { background-position: -640px -420px; }
+.emoji-1F512 { background-position: -640px -440px; }
+.emoji-1F513 { background-position: -640px -460px; }
+.emoji-1F514 { background-position: -640px -480px; }
+.emoji-1F515 { background-position: -640px -500px; }
+.emoji-1F516 { background-position: -640px -520px; }
+.emoji-1F517 { background-position: -640px -540px; }
+.emoji-1F518 { background-position: -640px -560px; }
+.emoji-1F519 { background-position: -640px -580px; }
+.emoji-1F51A { background-position: -640px -600px; }
+.emoji-1F51B { background-position: -640px -620px; }
+.emoji-1F51C { background-position: 0 -640px; }
+.emoji-1F51D { background-position: -20px -640px; }
+.emoji-1F51E { background-position: -40px -640px; }
+.emoji-1F51F { background-position: -60px -640px; }
+.emoji-1F520 { background-position: -80px -640px; }
+.emoji-1F521 { background-position: -100px -640px; }
+.emoji-1F522 { background-position: -120px -640px; }
+.emoji-1F523 { background-position: -140px -640px; }
+.emoji-1F524 { background-position: -160px -640px; }
+.emoji-1F525 { background-position: -180px -640px; }
+.emoji-1F526 { background-position: -200px -640px; }
+.emoji-1F527 { background-position: -220px -640px; }
+.emoji-1F528 { background-position: -240px -640px; }
+.emoji-1F529 { background-position: -260px -640px; }
+.emoji-1F52A { background-position: -280px -640px; }
+.emoji-1F52B { background-position: -300px -640px; }
+.emoji-1F52C { background-position: -320px -640px; }
+.emoji-1F52D { background-position: -340px -640px; }
+.emoji-1F52E { background-position: -360px -640px; }
+.emoji-1F52F { background-position: -380px -640px; }
+.emoji-1F530 { background-position: -400px -640px; }
+.emoji-1F531 { background-position: -420px -640px; }
+.emoji-1F532 { background-position: -440px -640px; }
+.emoji-1F533 { background-position: -460px -640px; }
+.emoji-1F534 { background-position: -480px -640px; }
+.emoji-1F535 { background-position: -500px -640px; }
+.emoji-1F536 { background-position: -520px -640px; }
+.emoji-1F537 { background-position: -540px -640px; }
+.emoji-1F538 { background-position: -560px -640px; }
+.emoji-1F539 { background-position: -580px -640px; }
+.emoji-1F53A { background-position: -600px -640px; }
+.emoji-1F53B { background-position: -620px -640px; }
+.emoji-1F53C { background-position: -640px -640px; }
+.emoji-1F53D { background-position: -660px 0; }
+.emoji-1F549 { background-position: -660px -20px; }
+.emoji-1F54A { background-position: -660px -40px; }
+.emoji-1F54B { background-position: -660px -60px; }
+.emoji-1F54C { background-position: -660px -80px; }
+.emoji-1F54D { background-position: -660px -100px; }
+.emoji-1F54E { background-position: -660px -120px; }
+.emoji-1F550 { background-position: -660px -140px; }
+.emoji-1F551 { background-position: -660px -160px; }
+.emoji-1F552 { background-position: -660px -180px; }
+.emoji-1F553 { background-position: -660px -200px; }
+.emoji-1F554 { background-position: -660px -220px; }
+.emoji-1F555 { background-position: -660px -240px; }
+.emoji-1F556 { background-position: -660px -260px; }
+.emoji-1F557 { background-position: -660px -280px; }
+.emoji-1F558 { background-position: -660px -300px; }
+.emoji-1F559 { background-position: -660px -320px; }
+.emoji-1F55A { background-position: -660px -340px; }
+.emoji-1F55B { background-position: -660px -360px; }
+.emoji-1F55C { background-position: -660px -380px; }
+.emoji-1F55D { background-position: -660px -400px; }
+.emoji-1F55E { background-position: -660px -420px; }
+.emoji-1F55F { background-position: -660px -440px; }
+.emoji-1F560 { background-position: -660px -460px; }
+.emoji-1F561 { background-position: -660px -480px; }
+.emoji-1F562 { background-position: -660px -500px; }
+.emoji-1F563 { background-position: -660px -520px; }
+.emoji-1F564 { background-position: -660px -540px; }
+.emoji-1F565 { background-position: -660px -560px; }
+.emoji-1F566 { background-position: -660px -580px; }
+.emoji-1F567 { background-position: -660px -600px; }
+.emoji-1F56F { background-position: -660px -620px; }
+.emoji-1F570 { background-position: -660px -640px; }
+.emoji-1F573 { background-position: 0 -660px; }
+.emoji-1F574 { background-position: -20px -660px; }
+.emoji-1F575 { background-position: -40px -660px; }
+.emoji-1F575-1F3FB { background-position: -60px -660px; }
+.emoji-1F575-1F3FC { background-position: -80px -660px; }
+.emoji-1F575-1F3FD { background-position: -100px -660px; }
+.emoji-1F575-1F3FE { background-position: -120px -660px; }
+.emoji-1F575-1F3FF { background-position: -140px -660px; }
+.emoji-1F576 { background-position: -160px -660px; }
+.emoji-1F577 { background-position: -180px -660px; }
+.emoji-1F578 { background-position: -200px -660px; }
+.emoji-1F579 { background-position: -220px -660px; }
+.emoji-1F57A { background-position: -240px -660px; }
+.emoji-1F57A-1F3FB { background-position: -260px -660px; }
+.emoji-1F57A-1F3FC { background-position: -280px -660px; }
+.emoji-1F57A-1F3FD { background-position: -300px -660px; }
+.emoji-1F57A-1F3FE { background-position: -320px -660px; }
+.emoji-1F57A-1F3FF { background-position: -340px -660px; }
+.emoji-1F587 { background-position: -360px -660px; }
+.emoji-1F58A { background-position: -380px -660px; }
+.emoji-1F58B { background-position: -400px -660px; }
+.emoji-1F58C { background-position: -420px -660px; }
+.emoji-1F58D { background-position: -440px -660px; }
+.emoji-1F590 { background-position: -460px -660px; }
+.emoji-1F590-1F3FB { background-position: -480px -660px; }
+.emoji-1F590-1F3FC { background-position: -500px -660px; }
+.emoji-1F590-1F3FD { background-position: -520px -660px; }
+.emoji-1F590-1F3FE { background-position: -540px -660px; }
+.emoji-1F590-1F3FF { background-position: -560px -660px; }
+.emoji-1F595 { background-position: -580px -660px; }
+.emoji-1F595-1F3FB { background-position: -600px -660px; }
+.emoji-1F595-1F3FC { background-position: -620px -660px; }
+.emoji-1F595-1F3FD { background-position: -640px -660px; }
+.emoji-1F595-1F3FE { background-position: -660px -660px; }
+.emoji-1F595-1F3FF { background-position: -680px 0; }
+.emoji-1F596 { background-position: -680px -20px; }
+.emoji-1F596-1F3FB { background-position: -680px -40px; }
+.emoji-1F596-1F3FC { background-position: -680px -60px; }
+.emoji-1F596-1F3FD { background-position: -680px -80px; }
+.emoji-1F596-1F3FE { background-position: -680px -100px; }
+.emoji-1F596-1F3FF { background-position: -680px -120px; }
+.emoji-1F5A4 { background-position: -680px -140px; }
+.emoji-1F5A5 { background-position: -680px -160px; }
+.emoji-1F5A8 { background-position: -680px -180px; }
+.emoji-1F5B1 { background-position: -680px -200px; }
+.emoji-1F5B2 { background-position: -680px -220px; }
+.emoji-1F5BC { background-position: -680px -240px; }
+.emoji-1F5C2 { background-position: -680px -260px; }
+.emoji-1F5C3 { background-position: -680px -280px; }
+.emoji-1F5C4 { background-position: -680px -300px; }
+.emoji-1F5D1 { background-position: -680px -320px; }
+.emoji-1F5D2 { background-position: -680px -340px; }
+.emoji-1F5D3 { background-position: -680px -360px; }
+.emoji-1F5DC { background-position: -680px -380px; }
+.emoji-1F5DD { background-position: -680px -400px; }
+.emoji-1F5DE { background-position: -680px -420px; }
+.emoji-1F5E1 { background-position: -680px -440px; }
+.emoji-1F5E3 { background-position: -680px -460px; }
+.emoji-1F5EF { background-position: -680px -480px; }
+.emoji-1F5F3 { background-position: -680px -500px; }
+.emoji-1F5FA { background-position: -680px -520px; }
+.emoji-1F5FB { background-position: -680px -540px; }
+.emoji-1F5FC { background-position: -680px -560px; }
+.emoji-1F5FD { background-position: -680px -580px; }
+.emoji-1F5FE { background-position: -680px -600px; }
+.emoji-1F5FF { background-position: -680px -620px; }
+.emoji-1F600 { background-position: -680px -640px; }
+.emoji-1F601 { background-position: -680px -660px; }
+.emoji-1F602 { background-position: 0 -680px; }
+.emoji-1F603 { background-position: -20px -680px; }
+.emoji-1F604 { background-position: -40px -680px; }
+.emoji-1F605 { background-position: -60px -680px; }
+.emoji-1F606 { background-position: -80px -680px; }
+.emoji-1F607 { background-position: -100px -680px; }
+.emoji-1F608 { background-position: -120px -680px; }
+.emoji-1F609 { background-position: -140px -680px; }
+.emoji-1F60A { background-position: -160px -680px; }
+.emoji-1F60B { background-position: -180px -680px; }
+.emoji-1F60C { background-position: -200px -680px; }
+.emoji-1F60D { background-position: -220px -680px; }
+.emoji-1F60E { background-position: -240px -680px; }
+.emoji-1F60F { background-position: -260px -680px; }
+.emoji-1F610 { background-position: -280px -680px; }
+.emoji-1F611 { background-position: -300px -680px; }
+.emoji-1F612 { background-position: -320px -680px; }
+.emoji-1F613 { background-position: -340px -680px; }
+.emoji-1F614 { background-position: -360px -680px; }
+.emoji-1F615 { background-position: -380px -680px; }
+.emoji-1F616 { background-position: -400px -680px; }
+.emoji-1F617 { background-position: -420px -680px; }
+.emoji-1F618 { background-position: -440px -680px; }
+.emoji-1F619 { background-position: -460px -680px; }
+.emoji-1F61A { background-position: -480px -680px; }
+.emoji-1F61B { background-position: -500px -680px; }
+.emoji-1F61C { background-position: -520px -680px; }
+.emoji-1F61D { background-position: -540px -680px; }
+.emoji-1F61E { background-position: -560px -680px; }
+.emoji-1F61F { background-position: -580px -680px; }
+.emoji-1F620 { background-position: -600px -680px; }
+.emoji-1F621 { background-position: -620px -680px; }
+.emoji-1F622 { background-position: -640px -680px; }
+.emoji-1F623 { background-position: -660px -680px; }
+.emoji-1F624 { background-position: -680px -680px; }
+.emoji-1F625 { background-position: -700px 0; }
+.emoji-1F626 { background-position: -700px -20px; }
+.emoji-1F627 { background-position: -700px -40px; }
+.emoji-1F628 { background-position: -700px -60px; }
+.emoji-1F629 { background-position: -700px -80px; }
+.emoji-1F62A { background-position: -700px -100px; }
+.emoji-1F62B { background-position: -700px -120px; }
+.emoji-1F62C { background-position: -700px -140px; }
+.emoji-1F62D { background-position: -700px -160px; }
+.emoji-1F62E { background-position: -700px -180px; }
+.emoji-1F62F { background-position: -700px -200px; }
+.emoji-1F630 { background-position: -700px -220px; }
+.emoji-1F631 { background-position: -700px -240px; }
+.emoji-1F632 { background-position: -700px -260px; }
+.emoji-1F633 { background-position: -700px -280px; }
+.emoji-1F634 { background-position: -700px -300px; }
+.emoji-1F635 { background-position: -700px -320px; }
+.emoji-1F636 { background-position: -700px -340px; }
+.emoji-1F637 { background-position: -700px -360px; }
+.emoji-1F638 { background-position: -700px -380px; }
+.emoji-1F639 { background-position: -700px -400px; }
+.emoji-1F63A { background-position: -700px -420px; }
+.emoji-1F63B { background-position: -700px -440px; }
+.emoji-1F63C { background-position: -700px -460px; }
+.emoji-1F63D { background-position: -700px -480px; }
+.emoji-1F63E { background-position: -700px -500px; }
+.emoji-1F63F { background-position: -700px -520px; }
+.emoji-1F640 { background-position: -700px -540px; }
+.emoji-1F641 { background-position: -700px -560px; }
+.emoji-1F642 { background-position: -700px -580px; }
+.emoji-1F643 { background-position: -700px -600px; }
+.emoji-1F644 { background-position: -700px -620px; }
+.emoji-1F645 { background-position: -700px -640px; }
+.emoji-1F645-1F3FB { background-position: -700px -660px; }
+.emoji-1F645-1F3FC { background-position: -700px -680px; }
+.emoji-1F645-1F3FD { background-position: 0 -700px; }
+.emoji-1F645-1F3FE { background-position: -20px -700px; }
+.emoji-1F645-1F3FF { background-position: -40px -700px; }
+.emoji-1F646 { background-position: -60px -700px; }
+.emoji-1F646-1F3FB { background-position: -80px -700px; }
+.emoji-1F646-1F3FC { background-position: -100px -700px; }
+.emoji-1F646-1F3FD { background-position: -120px -700px; }
+.emoji-1F646-1F3FE { background-position: -140px -700px; }
+.emoji-1F646-1F3FF { background-position: -160px -700px; }
+.emoji-1F647 { background-position: -180px -700px; }
+.emoji-1F647-1F3FB { background-position: -200px -700px; }
+.emoji-1F647-1F3FC { background-position: -220px -700px; }
+.emoji-1F647-1F3FD { background-position: -240px -700px; }
+.emoji-1F647-1F3FE { background-position: -260px -700px; }
+.emoji-1F647-1F3FF { background-position: -280px -700px; }
+.emoji-1F648 { background-position: -300px -700px; }
+.emoji-1F649 { background-position: -320px -700px; }
+.emoji-1F64A { background-position: -340px -700px; }
+.emoji-1F64B { background-position: -360px -700px; }
+.emoji-1F64B-1F3FB { background-position: -380px -700px; }
+.emoji-1F64B-1F3FC { background-position: -400px -700px; }
+.emoji-1F64B-1F3FD { background-position: -420px -700px; }
+.emoji-1F64B-1F3FE { background-position: -440px -700px; }
+.emoji-1F64B-1F3FF { background-position: -460px -700px; }
+.emoji-1F64C { background-position: -480px -700px; }
+.emoji-1F64C-1F3FB { background-position: -500px -700px; }
+.emoji-1F64C-1F3FC { background-position: -520px -700px; }
+.emoji-1F64C-1F3FD { background-position: -540px -700px; }
+.emoji-1F64C-1F3FE { background-position: -560px -700px; }
+.emoji-1F64C-1F3FF { background-position: -580px -700px; }
+.emoji-1F64D { background-position: -600px -700px; }
+.emoji-1F64D-1F3FB { background-position: -620px -700px; }
+.emoji-1F64D-1F3FC { background-position: -640px -700px; }
+.emoji-1F64D-1F3FD { background-position: -660px -700px; }
+.emoji-1F64D-1F3FE { background-position: -680px -700px; }
+.emoji-1F64D-1F3FF { background-position: -700px -700px; }
+.emoji-1F64E { background-position: -720px 0; }
+.emoji-1F64E-1F3FB { background-position: -720px -20px; }
+.emoji-1F64E-1F3FC { background-position: -720px -40px; }
+.emoji-1F64E-1F3FD { background-position: -720px -60px; }
+.emoji-1F64E-1F3FE { background-position: -720px -80px; }
+.emoji-1F64E-1F3FF { background-position: -720px -100px; }
+.emoji-1F64F { background-position: -720px -120px; }
+.emoji-1F64F-1F3FB { background-position: -720px -140px; }
+.emoji-1F64F-1F3FC { background-position: -720px -160px; }
+.emoji-1F64F-1F3FD { background-position: -720px -180px; }
+.emoji-1F64F-1F3FE { background-position: -720px -200px; }
+.emoji-1F64F-1F3FF { background-position: -720px -220px; }
+.emoji-1F680 { background-position: -720px -240px; }
+.emoji-1F681 { background-position: -720px -260px; }
+.emoji-1F682 { background-position: -720px -280px; }
+.emoji-1F683 { background-position: -720px -300px; }
+.emoji-1F684 { background-position: -720px -320px; }
+.emoji-1F685 { background-position: -720px -340px; }
+.emoji-1F686 { background-position: -720px -360px; }
+.emoji-1F687 { background-position: -720px -380px; }
+.emoji-1F688 { background-position: -720px -400px; }
+.emoji-1F689 { background-position: -720px -420px; }
+.emoji-1F68A { background-position: -720px -440px; }
+.emoji-1F68B { background-position: -720px -460px; }
+.emoji-1F68C { background-position: -720px -480px; }
+.emoji-1F68D { background-position: -720px -500px; }
+.emoji-1F68E { background-position: -720px -520px; }
+.emoji-1F68F { background-position: -720px -540px; }
+.emoji-1F690 { background-position: -720px -560px; }
+.emoji-1F691 { background-position: -720px -580px; }
+.emoji-1F692 { background-position: -720px -600px; }
+.emoji-1F693 { background-position: -720px -620px; }
+.emoji-1F694 { background-position: -720px -640px; }
+.emoji-1F695 { background-position: -720px -660px; }
+.emoji-1F696 { background-position: -720px -680px; }
+.emoji-1F697 { background-position: -720px -700px; }
+.emoji-1F698 { background-position: 0 -720px; }
+.emoji-1F699 { background-position: -20px -720px; }
+.emoji-1F69A { background-position: -40px -720px; }
+.emoji-1F69B { background-position: -60px -720px; }
+.emoji-1F69C { background-position: -80px -720px; }
+.emoji-1F69D { background-position: -100px -720px; }
+.emoji-1F69E { background-position: -120px -720px; }
+.emoji-1F69F { background-position: -140px -720px; }
+.emoji-1F6A0 { background-position: -160px -720px; }
+.emoji-1F6A1 { background-position: -180px -720px; }
+.emoji-1F6A2 { background-position: -200px -720px; }
+.emoji-1F6A3 { background-position: -220px -720px; }
+.emoji-1F6A3-1F3FB { background-position: -240px -720px; }
+.emoji-1F6A3-1F3FC { background-position: -260px -720px; }
+.emoji-1F6A3-1F3FD { background-position: -280px -720px; }
+.emoji-1F6A3-1F3FE { background-position: -300px -720px; }
+.emoji-1F6A3-1F3FF { background-position: -320px -720px; }
+.emoji-1F6A4 { background-position: -340px -720px; }
+.emoji-1F6A5 { background-position: -360px -720px; }
+.emoji-1F6A6 { background-position: -380px -720px; }
+.emoji-1F6A7 { background-position: -400px -720px; }
+.emoji-1F6A8 { background-position: -420px -720px; }
+.emoji-1F6A9 { background-position: -440px -720px; }
+.emoji-1F6AA { background-position: -460px -720px; }
+.emoji-1F6AB { background-position: -480px -720px; }
+.emoji-1F6AC { background-position: -500px -720px; }
+.emoji-1F6AD { background-position: -520px -720px; }
+.emoji-1F6AE { background-position: -540px -720px; }
+.emoji-1F6AF { background-position: -560px -720px; }
+.emoji-1F6B0 { background-position: -580px -720px; }
+.emoji-1F6B1 { background-position: -600px -720px; }
+.emoji-1F6B2 { background-position: -620px -720px; }
+.emoji-1F6B3 { background-position: -640px -720px; }
+.emoji-1F6B4 { background-position: -660px -720px; }
+.emoji-1F6B4-1F3FB { background-position: -680px -720px; }
+.emoji-1F6B4-1F3FC { background-position: -700px -720px; }
+.emoji-1F6B4-1F3FD { background-position: -720px -720px; }
+.emoji-1F6B4-1F3FE { background-position: -740px 0; }
+.emoji-1F6B4-1F3FF { background-position: -740px -20px; }
+.emoji-1F6B5 { background-position: -740px -40px; }
+.emoji-1F6B5-1F3FB { background-position: -740px -60px; }
+.emoji-1F6B5-1F3FC { background-position: -740px -80px; }
+.emoji-1F6B5-1F3FD { background-position: -740px -100px; }
+.emoji-1F6B5-1F3FE { background-position: -740px -120px; }
+.emoji-1F6B5-1F3FF { background-position: -740px -140px; }
+.emoji-1F6B6 { background-position: -740px -160px; }
+.emoji-1F6B6-1F3FB { background-position: -740px -180px; }
+.emoji-1F6B6-1F3FC { background-position: -740px -200px; }
+.emoji-1F6B6-1F3FD { background-position: -740px -220px; }
+.emoji-1F6B6-1F3FE { background-position: -740px -240px; }
+.emoji-1F6B6-1F3FF { background-position: -740px -260px; }
+.emoji-1F6B7 { background-position: -740px -280px; }
+.emoji-1F6B8 { background-position: -740px -300px; }
+.emoji-1F6B9 { background-position: -740px -320px; }
+.emoji-1F6BA { background-position: -740px -340px; }
+.emoji-1F6BB { background-position: -740px -360px; }
+.emoji-1F6BC { background-position: -740px -380px; }
+.emoji-1F6BD { background-position: -740px -400px; }
+.emoji-1F6BE { background-position: -740px -420px; }
+.emoji-1F6BF { background-position: -740px -440px; }
+.emoji-1F6C0 { background-position: -740px -460px; }
+.emoji-1F6C0-1F3FB { background-position: -740px -480px; }
+.emoji-1F6C0-1F3FC { background-position: -740px -500px; }
+.emoji-1F6C0-1F3FD { background-position: -740px -520px; }
+.emoji-1F6C0-1F3FE { background-position: -740px -540px; }
+.emoji-1F6C0-1F3FF { background-position: -740px -560px; }
+.emoji-1F6C1 { background-position: -740px -580px; }
+.emoji-1F6C2 { background-position: -740px -600px; }
+.emoji-1F6C3 { background-position: -740px -620px; }
+.emoji-1F6C4 { background-position: -740px -640px; }
+.emoji-1F6C5 { background-position: -740px -660px; }
+.emoji-1F6CB { background-position: -740px -680px; }
+.emoji-1F6CC { background-position: -740px -700px; }
+.emoji-1F6CD { background-position: -740px -720px; }
+.emoji-1F6CE { background-position: 0 -740px; }
+.emoji-1F6CF { background-position: -20px -740px; }
+.emoji-1F6D0 { background-position: -40px -740px; }
+.emoji-1F6D1 { background-position: -60px -740px; }
+.emoji-1F6D2 { background-position: -80px -740px; }
+.emoji-1F6E0 { background-position: -100px -740px; }
+.emoji-1F6E1 { background-position: -120px -740px; }
+.emoji-1F6E2 { background-position: -140px -740px; }
+.emoji-1F6E3 { background-position: -160px -740px; }
+.emoji-1F6E4 { background-position: -180px -740px; }
+.emoji-1F6E5 { background-position: -200px -740px; }
+.emoji-1F6E9 { background-position: -220px -740px; }
+.emoji-1F6EB { background-position: -240px -740px; }
+.emoji-1F6EC { background-position: -260px -740px; }
+.emoji-1F6F0 { background-position: -280px -740px; }
+.emoji-1F6F3 { background-position: -300px -740px; }
+.emoji-1F6F4 { background-position: -320px -740px; }
+.emoji-1F6F5 { background-position: -340px -740px; }
+.emoji-1F6F6 { background-position: -360px -740px; }
+.emoji-1F910 { background-position: -380px -740px; }
+.emoji-1F911 { background-position: -400px -740px; }
+.emoji-1F912 { background-position: -420px -740px; }
+.emoji-1F913 { background-position: -440px -740px; }
+.emoji-1F914 { background-position: -460px -740px; }
+.emoji-1F915 { background-position: -480px -740px; }
+.emoji-1F916 { background-position: -500px -740px; }
+.emoji-1F917 { background-position: -520px -740px; }
+.emoji-1F918 { background-position: -540px -740px; }
+.emoji-1F918-1F3FB { background-position: -560px -740px; }
+.emoji-1F918-1F3FC { background-position: -580px -740px; }
+.emoji-1F918-1F3FD { background-position: -600px -740px; }
+.emoji-1F918-1F3FE { background-position: -620px -740px; }
+.emoji-1F918-1F3FF { background-position: -640px -740px; }
+.emoji-1F919 { background-position: -660px -740px; }
+.emoji-1F919-1F3FB { background-position: -680px -740px; }
+.emoji-1F919-1F3FC { background-position: -700px -740px; }
+.emoji-1F919-1F3FD { background-position: -720px -740px; }
+.emoji-1F919-1F3FE { background-position: -740px -740px; }
+.emoji-1F919-1F3FF { background-position: -760px 0; }
+.emoji-1F91A { background-position: -760px -20px; }
+.emoji-1F91A-1F3FB { background-position: -760px -40px; }
+.emoji-1F91A-1F3FC { background-position: -760px -60px; }
+.emoji-1F91A-1F3FD { background-position: -760px -80px; }
+.emoji-1F91A-1F3FE { background-position: -760px -100px; }
+.emoji-1F91A-1F3FF { background-position: -760px -120px; }
+.emoji-1F91B { background-position: -760px -140px; }
+.emoji-1F91B-1F3FB { background-position: -760px -160px; }
+.emoji-1F91B-1F3FC { background-position: -760px -180px; }
+.emoji-1F91B-1F3FD { background-position: -760px -200px; }
+.emoji-1F91B-1F3FE { background-position: -760px -220px; }
+.emoji-1F91B-1F3FF { background-position: -760px -240px; }
+.emoji-1F91C { background-position: -760px -260px; }
+.emoji-1F91C-1F3FB { background-position: -760px -280px; }
+.emoji-1F91C-1F3FC { background-position: -760px -300px; }
+.emoji-1F91C-1F3FD { background-position: -760px -320px; }
+.emoji-1F91C-1F3FE { background-position: -760px -340px; }
+.emoji-1F91C-1F3FF { background-position: -760px -360px; }
+.emoji-1F91D { background-position: -760px -380px; }
+.emoji-1F91D-1F3FB { background-position: -760px -400px; }
+.emoji-1F91D-1F3FC { background-position: -760px -420px; }
+.emoji-1F91D-1F3FD { background-position: -760px -440px; }
+.emoji-1F91D-1F3FE { background-position: -760px -460px; }
+.emoji-1F91D-1F3FF { background-position: -760px -480px; }
+.emoji-1F91E { background-position: -760px -500px; }
+.emoji-1F91E-1F3FB { background-position: -760px -520px; }
+.emoji-1F91E-1F3FC { background-position: -760px -540px; }
+.emoji-1F91E-1F3FD { background-position: -760px -560px; }
+.emoji-1F91E-1F3FE { background-position: -760px -580px; }
+.emoji-1F91E-1F3FF { background-position: -760px -600px; }
+.emoji-1F920 { background-position: -760px -620px; }
+.emoji-1F921 { background-position: -760px -640px; }
+.emoji-1F922 { background-position: -760px -660px; }
+.emoji-1F923 { background-position: -760px -680px; }
+.emoji-1F924 { background-position: -760px -700px; }
+.emoji-1F925 { background-position: -760px -720px; }
+.emoji-1F926 { background-position: -760px -740px; }
+.emoji-1F926-1F3FB { background-position: 0 -760px; }
+.emoji-1F926-1F3FC { background-position: -20px -760px; }
+.emoji-1F926-1F3FD { background-position: -40px -760px; }
+.emoji-1F926-1F3FE { background-position: -60px -760px; }
+.emoji-1F926-1F3FF { background-position: -80px -760px; }
+.emoji-1F927 { background-position: -100px -760px; }
+.emoji-1F930 { background-position: -120px -760px; }
+.emoji-1F930-1F3FB { background-position: -140px -760px; }
+.emoji-1F930-1F3FC { background-position: -160px -760px; }
+.emoji-1F930-1F3FD { background-position: -180px -760px; }
+.emoji-1F930-1F3FE { background-position: -200px -760px; }
+.emoji-1F930-1F3FF { background-position: -220px -760px; }
+.emoji-1F933 { background-position: -240px -760px; }
+.emoji-1F933-1F3FB { background-position: -260px -760px; }
+.emoji-1F933-1F3FC { background-position: -280px -760px; }
+.emoji-1F933-1F3FD { background-position: -300px -760px; }
+.emoji-1F933-1F3FE { background-position: -320px -760px; }
+.emoji-1F933-1F3FF { background-position: -340px -760px; }
+.emoji-1F934 { background-position: -360px -760px; }
+.emoji-1F934-1F3FB { background-position: -380px -760px; }
+.emoji-1F934-1F3FC { background-position: -400px -760px; }
+.emoji-1F934-1F3FD { background-position: -420px -760px; }
+.emoji-1F934-1F3FE { background-position: -440px -760px; }
+.emoji-1F934-1F3FF { background-position: -460px -760px; }
+.emoji-1F935 { background-position: -480px -760px; }
+.emoji-1F935-1F3FB { background-position: -500px -760px; }
+.emoji-1F935-1F3FC { background-position: -520px -760px; }
+.emoji-1F935-1F3FD { background-position: -540px -760px; }
+.emoji-1F935-1F3FE { background-position: -560px -760px; }
+.emoji-1F935-1F3FF { background-position: -580px -760px; }
+.emoji-1F936 { background-position: -600px -760px; }
+.emoji-1F936-1F3FB { background-position: -620px -760px; }
+.emoji-1F936-1F3FC { background-position: -640px -760px; }
+.emoji-1F936-1F3FD { background-position: -660px -760px; }
+.emoji-1F936-1F3FE { background-position: -680px -760px; }
+.emoji-1F936-1F3FF { background-position: -700px -760px; }
+.emoji-1F937 { background-position: -720px -760px; }
+.emoji-1F937-1F3FB { background-position: -740px -760px; }
+.emoji-1F937-1F3FC { background-position: -760px -760px; }
+.emoji-1F937-1F3FD { background-position: -780px 0; }
+.emoji-1F937-1F3FE { background-position: -780px -20px; }
+.emoji-1F937-1F3FF { background-position: -780px -40px; }
+.emoji-1F938 { background-position: -780px -60px; }
+.emoji-1F938-1F3FB { background-position: -780px -80px; }
+.emoji-1F938-1F3FC { background-position: -780px -100px; }
+.emoji-1F938-1F3FD { background-position: -780px -120px; }
+.emoji-1F938-1F3FE { background-position: -780px -140px; }
+.emoji-1F938-1F3FF { background-position: -780px -160px; }
+.emoji-1F939 { background-position: -780px -180px; }
+.emoji-1F939-1F3FB { background-position: -780px -200px; }
+.emoji-1F939-1F3FC { background-position: -780px -220px; }
+.emoji-1F939-1F3FD { background-position: -780px -240px; }
+.emoji-1F939-1F3FE { background-position: -780px -260px; }
+.emoji-1F939-1F3FF { background-position: -780px -280px; }
+.emoji-1F93A { background-position: -780px -300px; }
+.emoji-1F93C { background-position: -780px -320px; }
+.emoji-1F93C-1F3FB { background-position: -780px -340px; }
+.emoji-1F93C-1F3FC { background-position: -780px -360px; }
+.emoji-1F93C-1F3FD { background-position: -780px -380px; }
+.emoji-1F93C-1F3FE { background-position: -780px -400px; }
+.emoji-1F93C-1F3FF { background-position: -780px -420px; }
+.emoji-1F93D { background-position: -780px -440px; }
+.emoji-1F93D-1F3FB { background-position: -780px -460px; }
+.emoji-1F93D-1F3FC { background-position: -780px -480px; }
+.emoji-1F93D-1F3FD { background-position: -780px -500px; }
+.emoji-1F93D-1F3FE { background-position: -780px -520px; }
+.emoji-1F93D-1F3FF { background-position: -780px -540px; }
+.emoji-1F93E { background-position: -780px -560px; }
+.emoji-1F93E-1F3FB { background-position: -780px -580px; }
+.emoji-1F93E-1F3FC { background-position: -780px -600px; }
+.emoji-1F93E-1F3FD { background-position: -780px -620px; }
+.emoji-1F93E-1F3FE { background-position: -780px -640px; }
+.emoji-1F93E-1F3FF { background-position: -780px -660px; }
+.emoji-1F940 { background-position: -780px -680px; }
+.emoji-1F941 { background-position: -780px -700px; }
+.emoji-1F942 { background-position: -780px -720px; }
+.emoji-1F943 { background-position: -780px -740px; }
+.emoji-1F944 { background-position: -780px -760px; }
+.emoji-1F945 { background-position: 0 -780px; }
+.emoji-1F947 { background-position: -20px -780px; }
+.emoji-1F948 { background-position: -40px -780px; }
+.emoji-1F949 { background-position: -60px -780px; }
+.emoji-1F94A { background-position: -80px -780px; }
+.emoji-1F94B { background-position: -100px -780px; }
+.emoji-1F950 { background-position: -120px -780px; }
+.emoji-1F951 { background-position: -140px -780px; }
+.emoji-1F952 { background-position: -160px -780px; }
+.emoji-1F953 { background-position: -180px -780px; }
+.emoji-1F954 { background-position: -200px -780px; }
+.emoji-1F955 { background-position: -220px -780px; }
+.emoji-1F956 { background-position: -240px -780px; }
+.emoji-1F957 { background-position: -260px -780px; }
+.emoji-1F958 { background-position: -280px -780px; }
+.emoji-1F959 { background-position: -300px -780px; }
+.emoji-1F95A { background-position: -320px -780px; }
+.emoji-1F95B { background-position: -340px -780px; }
+.emoji-1F95C { background-position: -360px -780px; }
+.emoji-1F95D { background-position: -380px -780px; }
+.emoji-1F95E { background-position: -400px -780px; }
+.emoji-1F980 { background-position: -420px -780px; }
+.emoji-1F981 { background-position: -440px -780px; }
+.emoji-1F982 { background-position: -460px -780px; }
+.emoji-1F983 { background-position: -480px -780px; }
+.emoji-1F984 { background-position: -500px -780px; }
+.emoji-1F985 { background-position: -520px -780px; }
+.emoji-1F986 { background-position: -540px -780px; }
+.emoji-1F987 { background-position: -560px -780px; }
+.emoji-1F988 { background-position: -580px -780px; }
+.emoji-1F989 { background-position: -600px -780px; }
+.emoji-1F98A { background-position: -620px -780px; }
+.emoji-1F98B { background-position: -640px -780px; }
+.emoji-1F98C { background-position: -660px -780px; }
+.emoji-1F98D { background-position: -680px -780px; }
+.emoji-1F98E { background-position: -700px -780px; }
+.emoji-1F98F { background-position: -720px -780px; }
+.emoji-1F990 { background-position: -740px -780px; }
+.emoji-1F991 { background-position: -760px -780px; }
+.emoji-1F9C0 { background-position: -780px -780px; }
+.emoji-203C { background-position: -800px 0; }
+.emoji-2049 { background-position: -800px -20px; }
+.emoji-2122 { background-position: -800px -40px; }
+.emoji-2139 { background-position: -800px -60px; }
+.emoji-2194 { background-position: -800px -80px; }
+.emoji-2195 { background-position: -800px -100px; }
+.emoji-2196 { background-position: -800px -120px; }
+.emoji-2197 { background-position: -800px -140px; }
+.emoji-2198 { background-position: -800px -160px; }
+.emoji-2199 { background-position: -800px -180px; }
+.emoji-21A9 { background-position: -800px -200px; }
+.emoji-21AA { background-position: -800px -220px; }
+.emoji-231A { background-position: -800px -240px; }
+.emoji-231B { background-position: -800px -260px; }
+.emoji-2328 { background-position: -800px -280px; }
+.emoji-23CF { background-position: -800px -300px; }
+.emoji-23E9 { background-position: -800px -320px; }
+.emoji-23EA { background-position: -800px -340px; }
+.emoji-23EB { background-position: -800px -360px; }
+.emoji-23EC { background-position: -800px -380px; }
+.emoji-23ED { background-position: -800px -400px; }
+.emoji-23EE { background-position: -800px -420px; }
+.emoji-23EF { background-position: -800px -440px; }
+.emoji-23F0 { background-position: -800px -460px; }
+.emoji-23F1 { background-position: -800px -480px; }
+.emoji-23F2 { background-position: -800px -500px; }
+.emoji-23F3 { background-position: -800px -520px; }
+.emoji-23F8 { background-position: -800px -540px; }
+.emoji-23F9 { background-position: -800px -560px; }
+.emoji-23FA { background-position: -800px -580px; }
+.emoji-24C2 { background-position: -800px -600px; }
+.emoji-25AA { background-position: -800px -620px; }
+.emoji-25AB { background-position: -800px -640px; }
+.emoji-25B6 { background-position: -800px -660px; }
+.emoji-25C0 { background-position: -800px -680px; }
+.emoji-25FB { background-position: -800px -700px; }
+.emoji-25FC { background-position: -800px -720px; }
+.emoji-25FD { background-position: -800px -740px; }
+.emoji-25FE { background-position: -800px -760px; }
+.emoji-2600 { background-position: -800px -780px; }
+.emoji-2601 { background-position: 0 -800px; }
+.emoji-2602 { background-position: -20px -800px; }
+.emoji-2603 { background-position: -40px -800px; }
+.emoji-2604 { background-position: -60px -800px; }
+.emoji-260E { background-position: -80px -800px; }
+.emoji-2611 { background-position: -100px -800px; }
+.emoji-2614 { background-position: -120px -800px; }
+.emoji-2615 { background-position: -140px -800px; }
+.emoji-2618 { background-position: -160px -800px; }
+.emoji-261D { background-position: -180px -800px; }
+.emoji-261D-1F3FB { background-position: -200px -800px; }
+.emoji-261D-1F3FC { background-position: -220px -800px; }
+.emoji-261D-1F3FD { background-position: -240px -800px; }
+.emoji-261D-1F3FE { background-position: -260px -800px; }
+.emoji-261D-1F3FF { background-position: -280px -800px; }
+.emoji-2620 { background-position: -300px -800px; }
+.emoji-2622 { background-position: -320px -800px; }
+.emoji-2623 { background-position: -340px -800px; }
+.emoji-2626 { background-position: -360px -800px; }
+.emoji-262A { background-position: -380px -800px; }
+.emoji-262E { background-position: -400px -800px; }
+.emoji-262F { background-position: -420px -800px; }
+.emoji-2638 { background-position: -440px -800px; }
+.emoji-2639 { background-position: -460px -800px; }
+.emoji-263A { background-position: -480px -800px; }
+.emoji-2648 { background-position: -500px -800px; }
+.emoji-2649 { background-position: -520px -800px; }
+.emoji-264A { background-position: -540px -800px; }
+.emoji-264B { background-position: -560px -800px; }
+.emoji-264C { background-position: -580px -800px; }
+.emoji-264D { background-position: -600px -800px; }
+.emoji-264E { background-position: -620px -800px; }
+.emoji-264F { background-position: -640px -800px; }
+.emoji-2650 { background-position: -660px -800px; }
+.emoji-2651 { background-position: -680px -800px; }
+.emoji-2652 { background-position: -700px -800px; }
+.emoji-2653 { background-position: -720px -800px; }
+.emoji-2660 { background-position: -740px -800px; }
+.emoji-2663 { background-position: -760px -800px; }
+.emoji-2665 { background-position: -780px -800px; }
+.emoji-2666 { background-position: -800px -800px; }
+.emoji-2668 { background-position: -820px 0; }
+.emoji-267B { background-position: -820px -20px; }
+.emoji-267F { background-position: -820px -40px; }
+.emoji-2692 { background-position: -820px -60px; }
+.emoji-2693 { background-position: -820px -80px; }
+.emoji-2694 { background-position: -820px -100px; }
+.emoji-2696 { background-position: -820px -120px; }
+.emoji-2697 { background-position: -820px -140px; }
+.emoji-2699 { background-position: -820px -160px; }
+.emoji-269B { background-position: -820px -180px; }
+.emoji-269C { background-position: -820px -200px; }
+.emoji-26A0 { background-position: -820px -220px; }
+.emoji-26A1 { background-position: -820px -240px; }
+.emoji-26AA { background-position: -820px -260px; }
+.emoji-26AB { background-position: -820px -280px; }
+.emoji-26B0 { background-position: -820px -300px; }
+.emoji-26B1 { background-position: -820px -320px; }
+.emoji-26BD { background-position: -820px -340px; }
+.emoji-26BE { background-position: -820px -360px; }
+.emoji-26C4 { background-position: -820px -380px; }
+.emoji-26C5 { background-position: -820px -400px; }
+.emoji-26C8 { background-position: -820px -420px; }
+.emoji-26CE { background-position: -820px -440px; }
+.emoji-26CF { background-position: -820px -460px; }
+.emoji-26D1 { background-position: -820px -480px; }
+.emoji-26D3 { background-position: -820px -500px; }
+.emoji-26D4 { background-position: -820px -520px; }
+.emoji-26E9 { background-position: -820px -540px; }
+.emoji-26EA { background-position: -820px -560px; }
+.emoji-26F0 { background-position: -820px -580px; }
+.emoji-26F1 { background-position: -820px -600px; }
+.emoji-26F2 { background-position: -820px -620px; }
+.emoji-26F3 { background-position: -820px -640px; }
+.emoji-26F4 { background-position: -820px -660px; }
+.emoji-26F5 { background-position: -820px -680px; }
+.emoji-26F7 { background-position: -820px -700px; }
+.emoji-26F8 { background-position: -820px -720px; }
+.emoji-26F9 { background-position: -820px -740px; }
+.emoji-26F9-1F3FB { background-position: -820px -760px; }
+.emoji-26F9-1F3FC { background-position: -820px -780px; }
+.emoji-26F9-1F3FD { background-position: -820px -800px; }
+.emoji-26F9-1F3FE { background-position: 0 -820px; }
+.emoji-26F9-1F3FF { background-position: -20px -820px; }
+.emoji-26FA { background-position: -40px -820px; }
+.emoji-26FD { background-position: -60px -820px; }
+.emoji-2702 { background-position: -80px -820px; }
+.emoji-2705 { background-position: -100px -820px; }
+.emoji-2708 { background-position: -120px -820px; }
+.emoji-2709 { background-position: -140px -820px; }
+.emoji-270A { background-position: -160px -820px; }
+.emoji-270A-1F3FB { background-position: -180px -820px; }
+.emoji-270A-1F3FC { background-position: -200px -820px; }
+.emoji-270A-1F3FD { background-position: -220px -820px; }
+.emoji-270A-1F3FE { background-position: -240px -820px; }
+.emoji-270A-1F3FF { background-position: -260px -820px; }
+.emoji-270B { background-position: -280px -820px; }
+.emoji-270B-1F3FB { background-position: -300px -820px; }
+.emoji-270B-1F3FC { background-position: -320px -820px; }
+.emoji-270B-1F3FD { background-position: -340px -820px; }
+.emoji-270B-1F3FE { background-position: -360px -820px; }
+.emoji-270B-1F3FF { background-position: -380px -820px; }
+.emoji-270C { background-position: -400px -820px; }
+.emoji-270C-1F3FB { background-position: -420px -820px; }
+.emoji-270C-1F3FC { background-position: -440px -820px; }
+.emoji-270C-1F3FD { background-position: -460px -820px; }
+.emoji-270C-1F3FE { background-position: -480px -820px; }
+.emoji-270C-1F3FF { background-position: -500px -820px; }
+.emoji-270D { background-position: -520px -820px; }
+.emoji-270D-1F3FB { background-position: -540px -820px; }
+.emoji-270D-1F3FC { background-position: -560px -820px; }
+.emoji-270D-1F3FD { background-position: -580px -820px; }
+.emoji-270D-1F3FE { background-position: -600px -820px; }
+.emoji-270D-1F3FF { background-position: -620px -820px; }
+.emoji-270F { background-position: -640px -820px; }
+.emoji-2712 { background-position: -660px -820px; }
+.emoji-2714 { background-position: -680px -820px; }
+.emoji-2716 { background-position: -700px -820px; }
+.emoji-271D { background-position: -720px -820px; }
+.emoji-2721 { background-position: -740px -820px; }
+.emoji-2728 { background-position: -760px -820px; }
+.emoji-2733 { background-position: -780px -820px; }
+.emoji-2734 { background-position: -800px -820px; }
+.emoji-2744 { background-position: -820px -820px; }
+.emoji-2747 { background-position: -840px 0; }
+.emoji-274C { background-position: -840px -20px; }
+.emoji-274E { background-position: -840px -40px; }
+.emoji-2753 { background-position: -840px -60px; }
+.emoji-2754 { background-position: -840px -80px; }
+.emoji-2755 { background-position: -840px -100px; }
+.emoji-2757 { background-position: -840px -120px; }
+.emoji-2763 { background-position: -840px -140px; }
+.emoji-2764 { background-position: -840px -160px; }
+.emoji-2795 { background-position: -840px -180px; }
+.emoji-2796 { background-position: -840px -200px; }
+.emoji-2797 { background-position: -840px -220px; }
+.emoji-27A1 { background-position: -840px -240px; }
+.emoji-27B0 { background-position: -840px -260px; }
+.emoji-27BF { background-position: -840px -280px; }
+.emoji-2934 { background-position: -840px -300px; }
+.emoji-2935 { background-position: -840px -320px; }
+.emoji-2B05 { background-position: -840px -340px; }
+.emoji-2B06 { background-position: -840px -360px; }
+.emoji-2B07 { background-position: -840px -380px; }
+.emoji-2B1B { background-position: -840px -400px; }
+.emoji-2B1C { background-position: -840px -420px; }
+.emoji-2B50 { background-position: -840px -440px; }
+.emoji-2B55 { background-position: -840px -460px; }
+.emoji-3030 { background-position: -840px -480px; }
+.emoji-303D { background-position: -840px -500px; }
+.emoji-3297 { background-position: -840px -520px; }
+.emoji-3299 { background-position: -840px -540px; }
+
+.emoji-icon {
+ background-image: image-url('emoji.png');
+ background-repeat: no-repeat;
+ height: 20px;
+ width: 20px;
+
+ @media only screen and (-webkit-min-device-pixel-ratio: 2),
+ only screen and (min--moz-device-pixel-ratio: 2),
+ only screen and (-o-min-device-pixel-ratio: 2/1),
+ only screen and (min-device-pixel-ratio: 2),
+ only screen and (min-resolution: 192dpi),
+ only screen and (min-resolution: 2dppx) {
+ background-image: image-url('emoji@2x.png');
+ background-size: 860px 840px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index f49d7b92a00..88ed0a4a17e 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -19,7 +19,7 @@
.file-title {
position: relative;
- background-color: $background-color;
+ background-color: $gray-light;
border-bottom: 1px solid $border-color;
margin: 0;
text-align: left;
@@ -59,10 +59,10 @@
}
.file-content {
- background: #fff;
+ background: $white-light;
&.image_file {
- background: #eee;
+ background: $file-image-bg;
text-align: center;
img {
@@ -84,8 +84,8 @@
}
&.blob-no-preview {
- background: #eee;
- text-shadow: 0 1px 2px #fff;
+ background: $blob-bg;
+ text-shadow: 0 1px 2px $white-light;
padding: 100px 0;
}
@@ -99,7 +99,7 @@
}
tr {
- border-bottom: 1px solid #eee;
+ border-bottom: 1px solid $blame-border;
}
td {
@@ -120,7 +120,7 @@
td.line-numbers {
float: none;
- border-left: 1px solid #ddd;
+ border-left: 1px solid $blame-line-numbers-border;
i {
float: none;
@@ -134,7 +134,7 @@
}
&.logs {
- background: #eee;
+ background: $logs-bg;
max-height: 700px;
overflow-y: auto;
@@ -143,14 +143,14 @@
padding: 10px 0;
border-left: 1px solid $border-color;
margin-bottom: 0;
- background: white;
+ background: $white-light;
li {
- color: #888;
+ color: $logs-li-color;
p {
margin: 0;
- color: #333;
+ color: $logs-p-color;
line-height: 24px;
padding-left: 10px;
}
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index a9006de6d3e..eadb9409fee 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -38,7 +38,7 @@
}
}
-@media (max-width: $screen-md-min) {
+@media (max-width: $screen-sm-max) {
ul.notes {
.flash-container.timeline-content {
margin-left: 0;
diff --git a/app/assets/stylesheets/framework/fonts.scss b/app/assets/stylesheets/framework/fonts.scss
deleted file mode 100644
index 5f9685bc71a..00000000000
--- a/app/assets/stylesheets/framework/fonts.scss
+++ /dev/null
@@ -1,45 +0,0 @@
-// Disabling "SpaceAfterPropertyColon" linter because the linter doesn't like
-// the way the `src` property is formatted in this file.
-// scss-lint:disable SpaceAfterPropertyColon
-
-/* latin-ext */
-@font-face {
- font-family: 'Source Sans Pro';
- font-style: normal;
- font-weight: 300;
- src:
- local('Source Sans Pro Light'),
- local('SourceSansPro-Light'),
- font-url('SourceSansPro-Light.ttf.woff2') format('woff2'),
- font-url('SourceSansPro-Light.ttf.woff') format('woff');
-}
-@font-face {
- font-family: 'Source Sans Pro';
- font-style: normal;
- font-weight: 400;
- src:
- local('Source Sans Pro'),
- local('SourceSansPro-Regular'),
- font-url('SourceSansPro-Regular.ttf.woff2') format('woff2'),
- font-url('SourceSansPro-Regular.ttf.woff') format('woff');
-}
-@font-face {
- font-family: 'Source Sans Pro';
- font-style: normal;
- font-weight: 600;
- src:
- local('Source Sans Pro Semibold'),
- local('SourceSansPro-Semibold'),
- font-url('SourceSansPro-Semibold.ttf.woff2') format('woff2'),
- font-url('SourceSansPro-Semibold.ttf.woff') format('woff');
-}
-@font-face {
- font-family: 'Source Sans Pro';
- font-style: normal;
- font-weight: 700;
- src:
- local('Source Sans Pro Bold'),
- local('SourceSansPro-Bold'),
- font-url('SourceSansPro-Bold.ttf.woff2') format('woff2'),
- font-url('SourceSansPro-Bold.ttf.woff') format('woff');
-}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index f0727e9688a..940807fc399 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -7,9 +7,9 @@ input {
}
input[type='text'].danger {
- background: #f2dede!important;
- border-color: #d66;
- text-shadow: 0 1px 1px #fff;
+ background: $input-danger-bg !important;
+ border-color: $input-danger-border;
+ text-shadow: 0 1px 1px $white-light;
}
.datetime-controls {
@@ -22,7 +22,7 @@ input[type='text'].danger {
margin-top: 0;
margin-bottom: -$gl-padding;
padding: $gl-padding;
- background-color: $background-color;
+ background-color: $gray-light;
border-top: 1px solid $border-color;
}
@@ -68,6 +68,46 @@ label {
}
}
+.help-form .form-group {
+ margin-left: 0;
+ margin-right: 0;
+
+ .control-label {
+ font-weight: bold;
+ padding-top: 4px;
+ }
+
+ .form-control {
+ height: 29px;
+ background: $white-light;
+ font-family: $monospace_font;
+ }
+
+ .input-group-btn .btn {
+ padding: 3px $gl-btn-padding;
+ background-color: $gray-light;
+ border: 1px solid $border-color;
+ }
+
+ .text-block {
+ line-height: 0.8;
+ padding-top: 9px;
+
+ code {
+ line-height: 1.8;
+ }
+ }
+
+ @media(max-width: $screen-xs-max) {
+ padding: 0 $gl-padding;
+
+ .control-label,
+ .text-block {
+ padding-left: 0;
+ }
+ }
+}
+
.fieldset-form fieldset {
margin-bottom: 20px;
}
@@ -109,7 +149,7 @@ label {
}
.form-control::-webkit-input-placeholder {
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
}
.input-group {
@@ -119,7 +159,7 @@ label {
}
.input-group-addon {
- background-color: #f7f8fa;
+ background-color: $input-group-addon-bg;
}
.input-group-addon:not(:first-child):not(:last-child) {
@@ -141,7 +181,7 @@ label {
border: 1px solid $green-normal;
&:focus {
- box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 $green-normal;
+ box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $green-normal;
border: 0 none;
}
}
@@ -150,7 +190,7 @@ label {
border: 1px solid $red-normal;
&:focus {
- box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 rgba(210, 40, 82, 0.6);
+ box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $gl-field-focus-shadow-error;
border: 0 none;
}
}
@@ -167,4 +207,3 @@ label {
color: $gl-text-color;
}
}
-
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 91ab1503439..5cd242af91d 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -21,7 +21,6 @@
background: $color-darker;
}
- .sidebar-header,
.sidebar-action-buttons {
color: $color-light;
background-color: lighten($color-darker, 5%);
@@ -86,37 +85,57 @@
}
$theme-charcoal: #3d454d;
+$theme-charcoal-light: #485157;
$theme-charcoal-dark: #383f45;
$theme-charcoal-text: #b9bbbe;
+$theme-blue-light: #becde9;
$theme-blue: #2980b9;
+$theme-blue-dark: #1970a9;
+$theme-blue-darker: #096099;
+
+$theme-graphite-lighter: #ccc;
+$theme-graphite-light: #777;
$theme-graphite: #666;
+$theme-graphite-dark: #555;
+
+$theme-gray-light: #979797;
$theme-gray: #373737;
+$theme-gray-dark: #272727;
+$theme-gray-darker: #222;
+
+$theme-green-light: #adc;
$theme-green: #019875;
+$theme-green-dark: #018865;
+$theme-green-darker: #017855;
+
+$theme-violet-light: #98c;
$theme-violet: #548;
+$theme-violet-dark: #436;
+$theme-violet-darker: #325;
body {
&.ui_blue {
- @include gitlab-theme(#becde9, $theme-blue, #1970a9, #096099);
+ @include gitlab-theme($theme-blue-light, $theme-blue, $theme-blue-dark, $theme-blue-darker);
}
&.ui_charcoal {
- @include gitlab-theme($theme-charcoal-text, #485157, $theme-charcoal, $theme-charcoal-dark);
+ @include gitlab-theme($theme-charcoal-text, $theme-charcoal-light, $theme-charcoal, $theme-charcoal-dark);
}
&.ui_graphite {
- @include gitlab-theme(#ccc, #777, $theme-graphite, #555);
+ @include gitlab-theme($theme-graphite-lighter, $theme-graphite-light, $theme-graphite, $theme-graphite-dark);
}
&.ui_gray {
- @include gitlab-theme(#979797, $theme-gray, #272727, #222);
+ @include gitlab-theme($theme-gray-light, $theme-gray, $theme-gray-dark, $theme-gray-darker);
}
&.ui_green {
- @include gitlab-theme(#adc, $theme-green, #018865, #017855);
+ @include gitlab-theme($theme-green-light, $theme-green, $theme-green-dark, $theme-green-darker);
}
&.ui_violet {
- @include gitlab-theme(#98c, $theme-violet, #436, #325);
+ @include gitlab-theme($theme-violet-light, $theme-violet, $theme-violet-dark, $theme-violet-darker);
}
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 4993ca7572a..971940773f7 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -8,8 +8,8 @@ header {
&.navbar-empty {
height: $header-height;
- background: #fff;
- border-bottom: 1px solid $btn-gray-hover;
+ background: $white-light;
+ border-bottom: 1px solid $white-normal;
.center-logo {
margin: 8px 0;
@@ -27,7 +27,7 @@ header {
z-index: 100;
margin-bottom: 0;
height: $header-height;
- background-color: $background-color;
+ background-color: $gray-light;
border: none;
border-bottom: 1px solid $border-color;
@@ -45,7 +45,7 @@ header {
padding: 0;
.nav > li > a {
- color: $gl-icon-color;
+ color: $gl-gray-light;
font-size: 18px;
padding: 0;
margin: ($header-height - 28) / 2 0;
@@ -62,16 +62,21 @@ header {
&:hover,
&:focus,
&:active {
- background-color: $background-color;
+ background-color: $gray-light;
+ color: darken($gl-gray-light, 30%);
+
+ .todos-pending-count {
+ background: darken($todo-alert-blue, 10%);
+ }
}
.fa-caret-down {
- font-size: 15px;
+ font-size: 14px;
}
}
.navbar-toggle {
- color: #666;
+ color: $nav-toggle-gray;
margin: 6px 0;
border-radius: 0;
position: absolute;
@@ -79,11 +84,11 @@ header {
padding: 6px 10px;
&:hover {
- background-color: $btn-gray-hover;
+ background-color: $white-normal;
}
&.active {
- color: $gl-icon-color;
+ color: $gl-gray-light;
}
}
}
@@ -95,14 +100,10 @@ header {
font-size: 18px;
padding: 6px 10px;
border: none;
- background-color: $background-color;
+ background-color: $gray-light;
&:hover {
- background-color: $btn-gray-hover;
- }
-
- &:focus {
- outline: none;
+ background-color: $white-normal;
}
}
}
@@ -155,8 +156,8 @@ header {
position: relative;
padding-right: 20px;
margin: 0;
- font-size: 19px;
- max-width: 400px;
+ font-size: 18px;
+ max-width: 385px;
display: inline-block;
line-height: $header-height;
font-weight: normal;
@@ -195,6 +196,10 @@ header {
font-size: 10px;
text-align: center;
cursor: pointer;
+
+ &:hover {
+ color: darken($color: $gl-text-color, $amount: 30%);
+ }
}
.project-item-select {
@@ -222,6 +227,14 @@ header {
}
}
+.page-sidebar-pinned.right-sidebar-expanded {
+ @media (max-width: $screen-md-max) {
+ .header-content .title {
+ width: 300px;
+ }
+ }
+}
+
@media (max-width: $screen-xs-max) {
header .container-fluid {
font-size: 18px;
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 07c8874bf03..909a0f4afda 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -11,7 +11,7 @@
border-radius: 0;
font-family: $monospace_font;
font-size: $code_font_size;
- line-height: $code_line_height !important;
+ line-height: 19px;
margin: 0;
overflow: auto;
overflow-y: hidden;
@@ -47,7 +47,7 @@
font-family: $monospace_font;
display: block;
font-size: $code_font_size !important;
- line-height: $code_line_height !important;
+ line-height: 19px;
white-space: nowrap;
i {
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
new file mode 100644
index 00000000000..226bd2ead31
--- /dev/null
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -0,0 +1,51 @@
+.ci-status-icon-success {
+ color: $gl-success;
+
+ svg {
+ fill: $gl-success;
+ }
+}
+
+.ci-status-icon-failed {
+ color: $gl-danger;
+
+ svg {
+ fill: $gl-danger;
+ }
+}
+
+.ci-status-icon-pending,
+.ci-status-icon-success_with_warnings {
+ color: $gl-warning;
+
+ svg {
+ fill: $gl-warning;
+ }
+}
+
+.ci-status-icon-running {
+ color: $blue-normal;
+
+ svg {
+ fill: $blue-normal;
+ }
+}
+
+.ci-status-icon-canceled,
+.ci-status-icon-disabled,
+.ci-status-icon-not-found {
+ color: $gl-gray;
+
+ svg {
+ fill: $gl-gray;
+ }
+}
+
+.ci-status-icon-created,
+.ci-status-icon-skipped {
+ color: $gray-darkest;
+
+ svg {
+ fill: $gray-darkest;
+ }
+}
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
new file mode 100644
index 00000000000..09a569ad415
--- /dev/null
+++ b/app/assets/stylesheets/framework/images.scss
@@ -0,0 +1,11 @@
+.appearance-logo-preview {
+ max-width: 400px;
+ margin-bottom: 20px;
+}
+
+.appearance-light-logo-preview {
+ background-color: $gray-light;
+ max-width: 72px;
+ padding: 10px;
+ margin-bottom: 10px;
+}
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index ba3930e03bd..298913108ee 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -20,7 +20,7 @@
display: block;
float: left;
margin-right: 10px;
- color: #fff;
+ color: $white-light;
font-size: $gl-font-size;
line-height: 25px;
@@ -37,6 +37,10 @@
}
&.status-box-expired {
- background: #cea61b;
+ background-color: $issue-status-expired;
+ }
+
+ &.status-box-upcoming {
+ background: $gl-gray-light;
}
}
diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss
index 30a5b837d69..18f2f316f02 100644
--- a/app/assets/stylesheets/framework/jquery.scss
+++ b/app/assets/stylesheets/framework/jquery.scss
@@ -4,13 +4,13 @@
&.ui-datepicker,
&.ui-datepicker-inline {
- border: 1px solid #ddd;
+ border: 1px solid $jq-ui-border;
padding: 10px;
width: 270px;
.ui-datepicker-header {
- background: #fff;
- border-color: #ddd;
+ background: $white-light;
+ border-color: $jq-ui-border;
.ui-datepicker-prev,
.ui-datepicker-next {
@@ -39,7 +39,7 @@
}
&.ui-autocomplete {
- border-color: #ddd;
+ border-color: $jq-ui-border;
padding: 0;
margin-top: 2px;
z-index: 1001;
@@ -50,9 +50,9 @@
}
.ui-state-default {
- border: 1px solid #fff;
- background: #fff;
- color: #777;
+ border: 1px solid $white-light;
+ background: $white-light;
+ color: $jq-ui-default-color;
}
.ui-state-highlight {
@@ -66,7 +66,7 @@
.ui-state-focus {
border: 1px solid $gl-primary;
background: $gl-primary;
- color: #fff;
+ color: $white-light;
}
}
}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 7baa4296abf..59fae61a44f 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -6,7 +6,7 @@ html {
body {
&.navless {
- background-color: white !important;
+ background-color: $white-light !important;
}
}
@@ -26,6 +26,45 @@ body {
.container-limited {
max-width: $fixed-layout-width;
+
+ &.limit-container-width {
+ max-width: $limited-layout-width;
+ }
+}
+
+.alert-wrapper {
+ margin-bottom: $gl-padding;
+
+ .alert {
+ margin-bottom: 0;
+ }
+
+ /* Stripe the background colors so that adjacent alert-warnings are distinct from one another */
+ .alert-warning {
+ transition: background-color 0.15s, border-color 0.15s;
+ background-color: lighten($gl-warning, 4%);
+ border-color: lighten($gl-warning, 4%);
+ }
+
+ .alert-warning + .alert-warning {
+ background-color: $gl-warning;
+ border-color: $gl-warning;
+ }
+
+ .alert-warning + .alert-warning + .alert-warning {
+ background-color: darken($gl-warning, 4%);
+ border-color: darken($gl-warning, 4%);
+ }
+
+ .alert-warning + .alert-warning + .alert-warning + .alert-warning {
+ background-color: darken($gl-warning, 8%);
+ border-color: darken($gl-warning, 8%);
+ }
+
+ .alert-warning:only-of-type {
+ background-color: $gl-warning;
+ border-color: $gl-warning;
+ }
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index bc0610cc417..e96cd671e34 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -11,8 +11,8 @@
> li {
padding: 10px 15px;
min-height: 20px;
- border-bottom: 1px solid #eee;
- border-bottom: 1px solid rgba(0, 0, 0, 0.05);
+ border-bottom: 1px solid $list-border-light;
+ border-bottom: 1px solid $list-border;
&::after {
content: " ";
@@ -21,7 +21,7 @@
}
&.disabled {
- color: #888;
+ color: $list-text-disabled-color;
}
&.unstyled {
@@ -31,12 +31,12 @@
}
&.warning-row {
- background-color: #fcf8e3;
- border-color: #faebcc;
- color: #8a6d3b;
+ background-color: $list-warning-row-bg;
+ border-color: $list-warning-row-border;
+ color: $list-warning-row-color;
}
- &.smoke { background-color: $background-color; }
+ &.smoke { background-color: $gray-light; }
&:not(.ui-sort-disabled):hover {
background: $row-hover;
@@ -46,7 +46,7 @@
border-bottom: none;
&.bottom {
- background: $background-color;
+ background: $gray-light;
}
}
@@ -59,7 +59,7 @@
p {
padding-top: 1px;
margin: 0;
- color: $gray-dark;
+ color: $white-normal;
img {
position: relative;
@@ -106,14 +106,14 @@ ul.task-list {
}
}
+// Generic content list
ul.content-list {
@include basic-list;
-
margin: 0;
padding: 0;
- > li {
- border-color: $table-border-color;
+ li {
+ border-color: $white-normal;
font-size: $list-font-size;
color: $list-text-color;
@@ -186,13 +186,48 @@ ul.content-list {
&.list-placeholder {
background-color: $gray-light;
- border: dotted 1px $gray-dark;
+ border: dotted 1px $white-normal;
margin: 1px 0;
min-height: 52px;
}
}
}
+// Content list using flexbox
+.flex-list {
+ .flex-row {
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ white-space: nowrap;
+ }
+
+ .row-main-content {
+ flex: 1 1 auto;
+ overflow: hidden;
+ padding-right: 8px;
+ }
+
+ .row-title {
+ font-weight: 600;
+ }
+
+ .row-second-line {
+ display: block;
+ }
+
+ .dropdown {
+ .btn-block {
+ margin-bottom: 0;
+ line-height: inherit;
+ }
+ }
+
+ .label-default {
+ color: $gl-gray-light;
+ }
+}
+
.panel > .content-list > li {
padding: $gl-padding-top $gl-padding;
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 6d28d98b283..e30d81d09f0 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -56,26 +56,24 @@
.md-header {
.nav-links {
- .active {
- a {
- border-bottom-color: #000;
- }
- }
-
a {
padding-top: 0;
- line-height: 1;
- border-bottom: 1px solid $border-color;
+ line-height: 19px;
&.btn.btn-xs {
padding: 2px 5px;
}
+
+ &:focus {
+ margin-top: -10px;
+ padding-top: 10px;
+ }
}
}
}
.referenced-users {
- color: #4c4e54;
+ color: $gl-header-color;
padding-top: 10px;
}
@@ -87,8 +85,8 @@
.markdown-area {
border-radius: 0;
- background: #fff;
- border: 1px solid #ddd;
+ background: $white-light;
+ border: 1px solid $md-area-border;
min-height: 140px;
max-height: 500px;
padding: 5px;
@@ -110,13 +108,13 @@
hr {
// Darken 'whitesmoke' a bit to make it more visible in note bodies
- border-color: darken(#f5f5f5, 8%);
+ border-color: darken($gray-normal, 8%);
margin: 10px 0;
}
// Border around images in issue and MR comments.
img:not(.emoji) {
- border: 1px solid $table-border-gray;
+ border: 1px solid $white-normal;
padding: 5px;
margin: 5px 0;
// Ensure that image does not exceed viewport
@@ -137,7 +135,7 @@
.toolbar-btn {
float: left;
padding: 0 5px;
- color: #959494;
+ color: $gl-gray-light;
background: transparent;
border: 0;
outline: 0;
@@ -148,7 +146,19 @@
}
}
-.atwho-view small.description {
- float: right;
- padding: 3px 5px;
+.atwho-view {
+ small.description {
+ float: right;
+ padding: 3px 5px;
+ }
+
+ .avatar-inline {
+ margin-bottom: 0;
+ }
+
+ .cur {
+ .avatar {
+ border: 1px solid $white-light;
+ }
+ }
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index f84ca36d10f..4f2ac77f228 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -24,7 +24,7 @@
@include clearfix;
padding: 10px 0;
- border-bottom: 1px solid #eee;
+ border-bottom: 1px solid $list-border-light;
display: block;
margin: 0;
@@ -67,8 +67,8 @@
}
@mixin dark-diff-match-line {
- color: rgba(255, 255, 255, 0.3);
- background: rgba(255, 255, 255, 0.1);
+ color: $dark-diff-match-bg;
+ background: $dark-diff-match-color;
}
@mixin webkit-prefix($property, $value) {
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index c1ed43bc20f..abfdd7a759d 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -71,7 +71,7 @@
display: none;
}
- .group-right-buttons {
+ .group-buttons {
display: none;
}
@@ -133,9 +133,9 @@
right: 0;
top: 30%;
padding: 5px 15px;
- background: #eee;
+ background: $show-aside-bg;
font-size: 20px;
- color: #777;
+ color: $show-aside-color;
z-index: 100;
- box-shadow: 0 1px 2px #ddd;
+ box-shadow: 0 1px 2px $show-aside-shadow;
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index fcaf5e18633..e4affbb1be1 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -49,51 +49,67 @@
padding: $gl-btn-padding;
padding-bottom: 11px;
margin-bottom: -1px;
- font-size: 15px;
+ font-size: 14px;
line-height: 28px;
- color: #959494;
+ color: $gl-gray-light;
border-bottom: 2px solid transparent;
&:hover,
&:active,
&:focus {
text-decoration: none;
- outline: none;
+ border-bottom: 2px solid $gray-darkest;
+ color: $black;
+
+ .badge {
+ color: $black;
+ }
}
}
&.active a {
border-bottom: 2px solid $link-underline-blue;
color: $black;
+ font-weight: 600;
+
+ .badge {
+ color: $black;
+ }
}
.badge {
font-weight: normal;
- background-color: #eee;
- color: $btn-transparent-color;
+ background-color: $nav-badge-bg;
+ color: $gl-gray-light;
vertical-align: baseline;
}
}
&.sub-nav {
text-align: center;
- background-color: $dark-background-color;
+ background-color: $gray-normal;
.container-fluid {
- background-color: $dark-background-color;
+ background-color: $gray-normal;
margin-bottom: 0;
}
li {
+ &.active a {
+ border-bottom: none;
+ color: $link-underline-blue;
+ }
+
a {
margin: 0;
padding: 11px 10px 9px;
- }
- &.active a {
- border-bottom: none;
- color: $link-underline-blue;
+ &:hover,
+ &:active,
+ &:focus {
+ border-bottom: none;
+ }
}
}
}
@@ -101,7 +117,7 @@
.top-area {
@include clearfix;
- border-bottom: 1px solid $btn-gray-hover;
+ border-bottom: 1px solid $white-normal;
.nav-text {
padding-top: 16px;
@@ -111,7 +127,7 @@
line-height: 28px;
/* Small devices (phones, tablets, 768px and lower) */
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
width: 100%;
}
}
@@ -256,6 +272,16 @@
width: auto;
}
}
+
+ &.multi-line {
+ .nav-text {
+ line-height: 20px;
+ }
+
+ .nav-controls {
+ padding: 17px 0;
+ }
+ }
}
.layout-nav {
@@ -263,7 +289,7 @@
top: $header-height;
width: 100%;
z-index: 11;
- background: $background-color;
+ background: $gray-light;
border-bottom: 1px solid $border-color;
transition: padding $sidebar-transition-duration;
text-align: center;
@@ -291,7 +317,7 @@
.fa-caret-down {
margin-left: 5px;
- color: $gl-icon-color;
+ color: $gl-gray-light;
}
.dropdown {
@@ -311,37 +337,9 @@
height: 51px;
li {
-
a {
padding-top: 10px;
}
-
- a,
- i {
- color: $layout-link-gray;
- }
-
- &.active {
-
- a,
- i {
- color: $black;
- }
-
- svg {
- path,
- polygon {
- fill: $black;
- }
- }
- }
-
- &:hover {
- a,
- i {
- color: $black;
- }
- }
}
}
}
@@ -354,7 +352,7 @@
}
.fade-right {
- @include fade(left, $background-color);
+ @include fade(left, $gray-light);
right: -5px;
.fa {
@@ -363,7 +361,7 @@
}
.fade-left {
- @include fade(right, $background-color);
+ @include fade(right, $gray-light);
left: -5px;
.fa {
@@ -374,7 +372,7 @@
&.sub-nav-scroll {
.fade-right {
- @include fade(left, $dark-background-color);
+ @include fade(left, $gray-normal);
right: 0;
.fa {
@@ -383,7 +381,7 @@
}
.fade-left {
- @include fade(right, $dark-background-color);
+ @include fade(right, $gray-normal);
left: 0;
.fa {
diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page-header.scss
new file mode 100644
index 00000000000..fff7d7f7524
--- /dev/null
+++ b/app/assets/stylesheets/framework/page-header.scss
@@ -0,0 +1,67 @@
+.page-content-header {
+ line-height: 34px;
+ padding: 10px 0;
+ margin-bottom: 0;
+
+ @media (min-width: $screen-sm-min) {
+ display: flex;
+ align-items: center;
+
+ .header-main-content {
+ flex: 1;
+ }
+ }
+
+ .header-action-buttons {
+ i {
+ color: $gl-gray-light;
+ font-size: 13px;
+ margin-right: 3px;
+ }
+
+ @media (max-width: $screen-xs-max) {
+ .btn {
+ width: 100%;
+ margin-top: 10px;
+ }
+
+ .dropdown {
+ width: 100%;
+ }
+ }
+ }
+
+ .avatar {
+ @extend .avatar-inline;
+ margin-left: 0;
+
+ @media (min-width: $screen-sm-min) {
+ margin-left: 4px;
+ }
+ }
+
+ .commit-committer-link,
+ .commit-author-link {
+ color: $gl-gray;
+ font-weight: bold;
+ }
+
+ .fa-clipboard {
+ color: $dropdown-title-btn-color;
+ }
+
+ .commit-info {
+ &.branches {
+ margin-left: 8px;
+ }
+ }
+
+ .ci-status-link {
+
+ svg {
+ position: relative;
+ top: 2px;
+ margin: 0 2px 0 3px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss
index cb2c351c368..b37c1d0d670 100644
--- a/app/assets/stylesheets/framework/pagination.scss
+++ b/app/assets/stylesheets/framework/pagination.scss
@@ -43,7 +43,7 @@
/**
* Small screen pagination
*/
-@media (max-width: $screen-xs) {
+@media (max-width: $screen-xs-min) {
.gl-pagination {
.pagination li a {
padding: 6px 10px;
@@ -62,7 +62,7 @@
/**
* Medium screen pagination
*/
-@media (min-width: $screen-xs) and (max-width: $screen-md-max) {
+@media (min-width: $screen-xs-min) and (max-width: $screen-md-max) {
.gl-pagination {
.page {
display: none;
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 920ce249b9a..9ab17e67d4c 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -6,7 +6,7 @@
.select2-container,
.select2-container.select2-drop-above {
.select2-choice {
- background: #fff;
+ background: $white-light;
border-color: $input-border;
height: 35px;
padding: $gl-vert-padding $gl-input-padding;
@@ -39,7 +39,7 @@
}
&:hover {
- background-color: $gray-dark;
+ background-color: $white-normal;
border-color: $border-white-normal;
color: $gl-text-color;
}
@@ -47,7 +47,7 @@
}
.select2-drop {
- box-shadow: rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0;
+ box-shadow: $select2-drop-shadow1 0 0 1px 0, $select2-drop-shadow2 0 2px 18px 0;
border-radius: $border-radius-default;
border: none;
min-width: 175px;
@@ -59,7 +59,7 @@
}
.select2-drop {
- color: #7f8fa4;
+ color: $gl-grayish-blue;
}
.select2-highlighted {
@@ -108,7 +108,7 @@
border-color: $input-border;
color: $gl-text-color;
line-height: 15px;
- background-color: $background-color;
+ background-color: $gray-light;
background-image: none;
.select2-search-choice-close {
@@ -156,7 +156,7 @@
.select2-search input {
padding: 2px 25px 2px 5px;
- background: #fff image-url('select2.png');
+ background: $white-light image-url('select2.png');
background-repeat: no-repeat;
background-position: right 0 bottom 6px;
border: 1px solid $input-border;
@@ -169,7 +169,7 @@
}
.select2-search input.select2-active {
- background-color: #fff;
+ background-color: $white-light;
background-image: image-url('select2-spinner.gif') !important;
background-repeat: no-repeat;
background-position: right 5px center !important;
@@ -206,7 +206,7 @@
.select2-highlighted {
.group-result {
.group-path {
- color: #fff;
+ color: $white-light;
}
}
}
@@ -221,7 +221,7 @@
}
.group-path {
- color: #999;
+ color: $group-path-color;
}
}
@@ -241,7 +241,7 @@
.namespace-result {
.namespace-kind {
- color: #aaa;
+ color: $namespace-kind-color;
font-weight: normal;
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index d74c14ee2a4..46a06cd7eab 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -23,7 +23,7 @@
.sidebar-wrapper {
z-index: 1000;
- background: $background-color;
+ background: $gray-light;
.nicescroll-rails-hr {
// TODO: Figure out why nicescroll doesn't hide horizontal bar
@@ -36,7 +36,7 @@
transition: padding $sidebar-transition-duration;
.container-fluid {
- background: #fff;
+ background: $white-light;
padding: 0 $gl-padding;
&.container-blank {
@@ -59,11 +59,6 @@
padding: 0 !important;
}
- .sidebar-header {
- padding: 11px 22px 12px;
- font-size: 20px;
- }
-
li {
&.separate-item {
padding-top: 10px;
@@ -83,7 +78,6 @@
display: block;
text-decoration: none;
font-weight: normal;
- outline: none;
&:hover,
&:active,
@@ -221,7 +215,7 @@ header.header-sidebar-pinned {
padding-right: 0;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
- &:not(.build-sidebar) {
+ &:not(.build-sidebar):not(.wiki-sidebar) {
padding-right: $sidebar_collapsed_width;
}
}
diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss
new file mode 100644
index 00000000000..5f7e1b17cc7
--- /dev/null
+++ b/app/assets/stylesheets/framework/snippets.scss
@@ -0,0 +1,48 @@
+.snippet-row {
+ .title {
+ margin-bottom: 2px;
+ }
+
+ .snippet-filename {
+ padding: 0 2px;
+ }
+}
+
+.snippet-form-holder .file-holder .file-title {
+ padding: 2px;
+}
+
+.markdown-snippet-copy {
+ position: fixed;
+ top: -10px;
+ left: -10px;
+ max-height: 0;
+ max-width: 0;
+}
+
+.snippet-file-content {
+ border-radius: 3px;
+}
+
+.snippet-header {
+ padding: $gl-padding 0;
+}
+
+.snippet-title {
+ font-size: 24px;
+ font-weight: 600;
+}
+
+.snippet-edited-ago {
+ color: $gray-darkest;
+}
+
+.snippet-actions {
+ @media (min-width: $screen-sm-min) {
+ float: right;
+ }
+}
+
+.snippet-scope-menu .btn-new {
+ margin-top: 15px;
+}
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 9a90d3794fd..6d9fa74a030 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -14,11 +14,11 @@ table {
.warning,
.danger,
.info {
- color: #fff;
+ color: $white-light;
a:not(.btn) {
text-decoration: underline;
- color: #fff;
+ color: $white-light;
}
}
@@ -31,14 +31,31 @@ table {
}
th {
- background-color: $background-color;
+ background-color: $gray-light;
font-weight: normal;
border-bottom: none;
+
+ &.wide {
+ width: 55%;
+ }
}
td {
- border-color: $table-border-color;
+ border-color: $white-normal;
}
}
}
}
+
+.responsive-table {
+ @media (max-width: $screen-sm-max) {
+ th {
+ width: 100%;
+ }
+
+ td {
+ width: 100%;
+ float: left;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 875cded8b4e..6078505807e 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -6,7 +6,7 @@
.timeline-entry {
padding: $gl-padding $gl-btn-padding 11px;
- border-color: $table-border-color;
+ border-color: $white-normal;
color: $gl-gray;
border-bottom: 1px solid $border-white-light;
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index 59f4594bb83..718dbbfea27 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -33,7 +33,7 @@
@import "bootstrap/labels";
@import "bootstrap/badges";
@import "bootstrap/alerts";
-@import "bootstrap/progress-bars";
+// @import "bootstrap/progress-bars";
@import "bootstrap/list-group";
@import "bootstrap/wells";
@import "bootstrap/close";
@@ -97,13 +97,13 @@
display: inline-block;
&.label-gray {
- background-color: #f8fafc;
+ background-color: $label-gray-bg;
color: $gl-gray;
text-shadow: none;
}
&.label-inverse {
- background-color: #333;
+ background-color: $label-inverse-bg;
}
}
@@ -158,7 +158,7 @@
font-weight: normal;
a {
- color: #777;
+ color: $panel-heading-link-color;
}
}
}
@@ -172,7 +172,7 @@
.alert {
a:not(.btn) {
@extend .alert-link;
- color: #fff;
+ color: $white-light;
text-decoration: underline;
}
}
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index 44fe37d3a4a..876adf7f712 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -48,7 +48,7 @@ $font-size-base: $gl-font-size;
$padding-base-vertical: $gl-vert-padding;
$padding-base-horizontal: $gl-padding;
-$component-active-color: #fff;
+$component-active-color: $white-light;
$component-active-bg: $brand-info;
//== Forms
@@ -66,7 +66,7 @@ $legend-color: $text-color;
//##
$pagination-color: $gl-gray;
-$pagination-bg: #fff;
+$pagination-bg: $white-light;
$pagination-border: $border-color;
$pagination-hover-color: $gl-gray;
@@ -74,11 +74,11 @@ $pagination-hover-bg: $row-hover;
$pagination-hover-border: $border-color;
$pagination-active-color: $blue-dark;
-$pagination-active-bg: #fff;
+$pagination-active-bg: $white-light;
$pagination-active-border: $border-color;
$pagination-disabled-color: #cdcdcd;
-$pagination-disabled-bg: $background-color;
+$pagination-disabled-bg: $gray-light;
$pagination-disabled-border: $border-color;
@@ -86,19 +86,19 @@ $pagination-disabled-border: $border-color;
//
//## Define colors for form feedback states and, by default, alerts.
-$state-success-text: #fff;
+$state-success-text: $white-light;
$state-success-bg: $brand-success;
$state-success-border: $brand-success;
-$state-info-text: #fff;
+$state-info-text: $white-light;
$state-info-bg: $brand-info;
$state-info-border: $brand-info;
-$state-warning-text: #fff;
+$state-warning-text: $white-light;
$state-warning-bg: $brand-warning;
$state-warning-border: $brand-warning;
-$state-danger-text: #fff;
+$state-danger-text: $white-light;
$state-danger-bg: $brand-danger;
$state-danger-border: $brand-danger;
@@ -117,8 +117,8 @@ $alert-border-radius: 0;
$panel-border-radius: 2px;
$panel-default-text: $text-color;
$panel-default-border: $border-color;
-$panel-default-heading-bg: $background-color;
-$panel-footer-bg: $background-color;
+$panel-default-heading-bg: $gray-light;
+$panel-footer-bg: $gray-light;
$panel-inner-border: $border-color;
//== Wells
@@ -135,14 +135,14 @@ $well-border: #eee;
$code-color: #c7254e;
$code-bg: #f9f2f4;
-$kbd-color: #fff;
+$kbd-color: $white-light;
$kbd-bg: #333;
//== Buttons
//
//##
$btn-default-color: $gl-text-color;
-$btn-default-bg: #fff;
+$btn-default-bg: $white-light;
$btn-default-border: #e7e9ed;
//== Nav
@@ -153,8 +153,8 @@ $nav-link-padding: 13px $gl-padding;
//== Code
//
//##
-$pre-bg: $background-color !default;
+$pre-bg: $gray-light !default;
$pre-color: $gl-gray !default;
$pre-border-color: $border-color;
-$table-bg-accent: $background-color;
+$table-bg-accent: $gray-light;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 070e42d63d2..d906d26bba9 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -33,15 +33,15 @@
padding: 3px 5px;
font-size: 11px;
line-height: 10px;
- color: #555;
+ color: $kdb-color;
vertical-align: middle;
- background-color: #fcfcfc;
+ background-color: $kdb-bg;
border-width: 1px;
border-style: solid;
- border-color: #ccc #ccc #bbb;
+ border-color: $kdb-border $kdb-border $kdb-border-bottom;
border-image: none;
border-radius: 3px;
- box-shadow: 0 -1px 0 #bbb inset;
+ box-shadow: 0 -1px 0 $kdb-shadow inset;
}
h1 {
@@ -81,7 +81,7 @@
}
blockquote {
- color: #7f8fa4;
+ color: $gl-grayish-blue;
font-size: inherit;
padding: 8px 21px;
margin: 12px 0;
@@ -94,13 +94,13 @@
}
blockquote p {
- color: #7f8fa4 !important;
+ color: $gl-grayish-blue !important;
font-size: inherit;
line-height: 1.5;
}
p {
- color: #5c5d5e;
+ color: $gl-text-color-dark;
margin: 6px 0 0;
}
@@ -108,10 +108,10 @@
@extend .table;
@extend .table-bordered;
margin: 12px 0;
- color: #5c5d5e;
+ color: $gl-text-color-dark;
th {
- background: #f8fafc;
+ background: $label-gray-bg;
}
}
@@ -182,6 +182,7 @@
left: -16px;
position: absolute;
text-decoration: none;
+ outline: none;
&::after {
content: image-url('icon_anchor.svg');
@@ -201,7 +202,7 @@
*
*/
body {
- -webkit-text-shadow: rgba(255,255,255,0.01) 0 0 1px;
+ -webkit-text-shadow: $body-text-shadow 0 0 1px;
}
.page-title {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index e0d00759c9c..d0c27d64239 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -12,70 +12,68 @@ $sidebar-breakpoint: 1024px;
/*
* Color schema
*/
+$darken-normal-factor: 7%;
+$darken-dark-factor: 10%;
+$darken-border-factor: 5%;
+
$white-light: #fff;
-$white-normal: #ededed;
-$white-dark: #ececec;
+$white-normal: #f0f0f0;
+$white-dark: #eaeaea;
$gray-lightest: #fdfdfd;
$gray-light: #fafafa;
$gray-lighter: #f9f9f9;
$gray-normal: #f5f5f5;
-$gray-dark: #ededed;
$gray-darker: #eee;
-$gray-darkest: #c9c9c9;
+$gray-darkest: #c4c4c4;
-$green-light: #38ae67;
-$green-normal: #2faa60;
-$green-dark: #2ca05b;
+$green-light: #3cbd70;
+$green-normal: darken($green-light, $darken-normal-factor);
+$green-dark: darken($green-light, $darken-dark-factor);
$blue-light: #2ea8e5;
-$blue-normal: #2d9fd8;
-$blue-dark: #2897ce;
+$blue-normal: darken($blue-light, $darken-normal-factor);
+$blue-dark: darken($blue-light, $darken-dark-factor);
$blue-medium-light: #3498cb;
-$blue-medium: #2f8ebf;
-$blue-medium-dark: #2d86b4;
+$blue-medium: darken($blue-medium-light, $darken-normal-factor);
+$blue-medium-dark: darken($blue-medium-light, $darken-dark-factor);
$blue-light-transparent: rgba(44, 159, 216, 0.05);
$orange-light: #fc8a51;
-$orange-normal: #e75e40;
-$orange-dark: #ce5237;
+$orange-normal: darken($orange-light, $darken-normal-factor);
+$orange-dark: darken($orange-light, $darken-dark-factor);
$red-light: #e52c5a;
-$red-normal: #d22852;
-$red-dark: darken($red-normal, 5%);
+$red-normal: darken($red-light, $darken-normal-factor);
+$red-dark: darken($red-light, $darken-dark-factor);
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
-$border-white-light: #f1f2f4;
-$border-white-normal: #d6dae2;
-$border-white-dark: #c6cacf;
+$border-white-light: darken($white-light, $darken-border-factor);
+$border-white-normal: darken($white-normal, $darken-border-factor);
-$border-gray-light: #dcdcdc;
-$border-gray-normal: #d7d7d7;
-$border-gray-dark: #c6cacf;
+$border-gray-normal: darken($gray-normal, $darken-border-factor);
+$border-gray-dark: darken($white-normal, $darken-border-factor);
$border-green-extra-light: #9adb84;
-$border-green-light: #2faa60;
-$border-green-normal: #2ca05b;
-$border-green-dark: #279654;
-
-$border-blue-light: #2d9fd8;
-$border-blue-normal: #2897ce;
-$border-blue-dark: #258dc1;
+$border-green-light: darken($green-light, $darken-border-factor);
+$border-green-normal: darken($green-normal, $darken-border-factor);
+$border-green-dark: darken($green-dark, $darken-border-factor);
-$border-orange-light: #fc6d26;
-$border-orange-normal: #ce5237;
-$border-orange-dark: #c14e35;
+$border-blue-light: darken($blue-light, $darken-border-factor);
+$border-blue-normal: darken($blue-normal, $darken-border-factor);
+$border-blue-dark: darken($blue-dark, $darken-border-factor);
-$border-red-light: #d22852;
-$border-red-normal: #ca264f;
-$border-red-dark: darken($border-red-normal, 5%);
+$border-orange-light: darken($orange-light, $darken-border-factor);
+$border-orange-normal: darken($orange-normal, $darken-border-factor);
+$border-orange-dark: darken($orange-dark, $darken-border-factor);
-$help-well-bg: $gray-light;
-$help-well-border: #e5e5e5;
+$border-red-light: darken($red-light, $darken-border-factor);
+$border-red-normal: darken($red-normal, $darken-border-factor);
+$border-red-dark: darken($red-dark, $darken-border-factor);
$warning-message-bg: #fbf2d9;
$warning-message-color: #9e8e60;
@@ -86,31 +84,29 @@ $warning-message-border: #f0e2bb;
*/
$border-color: #e5e5e5;
$focus-border-color: #3aabf0;
-$table-border-color: #f0f0f0;
-$background-color: $gray-light;
-$dark-background-color: #f5f5f5;
-$table-text-gray: #8f8f8f;
-$widget-expand-item: #e8f2f7;
-$widget-inner-border: #eef0f2;
+$well-expand-item: #e8f2f7;
+$well-inner-border: #eef0f2;
+$well-light-border: #f1f1f1;
+$well-light-text-color: #5b6169;
/*
* Text
*/
-$gl-font-size: 15px;
+$gl-font-size: 14px;
$gl-title-color: #333;
$gl-text-color: #5c5c5c;
+$gl-text-color-dark: #5c5d5e;
$gl-text-color-light: #8c8c8c;
$gl-text-green: #4a2;
$gl-text-red: #d12f19;
$gl-text-orange: #d90;
$gl-link-color: #3777b0;
+$gl-diff-text-color: #555;
$gl-dark-link-color: #333;
-$gl-placeholder-color: #8f8f8f;
-$gl-icon-color: $gl-placeholder-color;
+$gl-gray-light: #8f8f8f;
$gl-grayish-blue: #7f8fa4;
$gl-gray: $gl-text-color;
$gl-gray-dark: #313236;
-$gl-gray-light: $gl-placeholder-color;
$gl-header-color: #4c4e54;
/*
@@ -119,13 +115,20 @@ $gl-header-color: #4c4e54;
$list-font-size: $gl-font-size;
$list-title-color: $gl-title-color;
$list-text-color: $gl-text-color;
+$list-text-disabled-color: #888;
+$list-border-light: #eee;
+$list-border: rgba(0, 0, 0, 0.05);
$list-text-height: 42px;
+$list-warning-row-bg: #fcf8e3;
+$list-warning-row-border: #faebcc;
+$list-warning-row-color: #8a6d3b;
/*
* Markdown
*/
$md-text-color: $gl-text-color;
$md-link-color: $gl-link-color;
+$md-area-border: #ddd;
/*
* Code
@@ -149,22 +152,57 @@ $gl-sidebar-padding: 22px;
$row-hover: #f7faff;
$row-hover-border: #b2d7ff;
$progress-color: #c0392b;
-$avatar_radius: 50%;
$header-height: 50px;
$fixed-layout-width: 1280px;
+$limited-layout-width: 990px;
$gl-avatar-size: 40px;
$error-exclamation-point: #e62958;
$border-radius-default: 2px;
-$btn-transparent-color: #8f8f8f;
$settings-icon-size: 18px;
-$provider-btn-group-border: #e5e5e5;
$provider-btn-not-active-color: #4688f1;
$link-underline-blue: #4a8bee;
+$active-item-blue: #4a8bee;
$layout-link-gray: #7e7c7c;
-$todo-alert-blue: #428bca;
$btn-side-margin: 10px;
$btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px;
+$issue-status-expired: #cea61b;
+$issuable-sidebar-color: #999;
+$issuable-avatar-hover-border: #999;
+$issuable-clipboard-color: #999;
+$show-aside-bg: #eee;
+$show-aside-color: #777;
+$show-aside-shadow: #ddd;
+$group-path-color: #999;
+$namespace-kind-color: #aaa;
+$panel-heading-link-color: #777;
+$graph-author-email-color: #777;
+$count-arrow-border: #dce0e5;
+$save-project-loader-color: #555;
+$divergence-graph-bar-bg: #ccc;
+$divergence-graph-separator-bg: #ccc;
+
+/*
+* Common component specific colors
+*/
+$hint-color: #999;
+$well-pre-bg: #eee;
+$well-pre-color: #555;
+$loading-color: #555;
+$update-author-color: #999;
+$user-mention-color: #2fa0bb;
+$time-color: #999;
+$project-member-show-color: #aaa;
+$gl-promo-color: #aaa;
+$error-bg: #c67;
+$warning-message-bg: #ffffe6;
+$warning-message-border: #ed9;
+$warning-message-color: #b90;
+$control-group-descr-color: #666;
+$table-permission-x-bg: #d9edf7;
+$username-color: #666;
+$description-color: #666;
+$profiler-border: #eee;
/* tanuki logo colors */
$tanuki-red: #e24329;
@@ -195,101 +233,299 @@ $line-removed-dark: #fac5cd;
$line-number-old: #f9d7dc;
$line-number-new: #ddfbe6;
$line-number-select: #fbf2da;
-$match-line: $gray-light;
-$table-border-gray: #f0f0f0;
$line-target-blue: #f6faff;
$line-select-yellow: #fcf8e7;
$line-select-yellow-dark: #f0e2bd;
+$dark-diff-match-bg: rgba(255, 255, 255, 0.3);
+$dark-diff-match-color: rgba(255, 255, 255, 0.1);
+$file-mode-changed: #777;
+$file-mode-changed: #777;
+$diff-image-bg: #ddd;
+$diff-image-info-color: grey;
+$diff-swipe-border: #999;
+$diff-view-modes-color: grey;
+$diff-view-modes-border: #c1c1c1;
/*
* Fonts
*/
$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
-$regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif;
+$regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
/*
* Dropdowns
*/
$dropdown-width: 300px;
-$dropdown-bg: #fff;
$dropdown-link-color: #555;
$dropdown-link-hover-bg: $row-hover;
$dropdown-empty-row-bg: rgba(#000, .04);
-$dropdown-border-color: rgba(#000, .1);
+$dropdown-border-color: $border-color;
$dropdown-shadow-color: rgba(#000, .1);
$dropdown-divider-color: rgba(#000, .1);
-$dropdown-header-color: #959494;
$dropdown-title-btn-color: #bfbfbf;
$dropdown-input-color: #555;
+$dropdown-input-fa-color: #c7c7c7;
$dropdown-input-focus-border: $focus-border-color;
$dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4);
$dropdown-loading-bg: rgba(#fff, .6);
+$dropdown-chevron-size: 10px;
+$dropdown-toggle-active-border-color: darken($border-color, 14%);
-$dropdown-toggle-bg: #fff;
-$dropdown-toggle-color: #626262;
-$dropdown-toggle-border-color: #eaeaea;
-$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%);
-$dropdown-toggle-icon-color: #c4c4c4;
-$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
/*
* Buttons
*/
$btn-active-gray: #ececec;
-$btn-placeholder-gray: #c7c7c7;
-$btn-white-active: #848484;
-$btn-gray-hover: #eee;
+$btn-active-gray-light: e4e7ed;
/*
* Award emoji
*/
-$award-emoji-menu-bg: #fff;
-$award-emoji-menu-border: #f1f2f4;
-$award-emoji-new-btn-icon-color: #dcdcdc;
+$award-emoji-menu-shadow: rgba(0,0,0,.175);
/*
* Search Box
*/
$search-input-border-color: rgba(#4688f1, .8);
$search-input-focus-shadow-color: $dropdown-input-focus-shadow;
-$search-input-width: 244px;
-$location-badge-color: #aaa;
-$location-badge-bg: $gray-normal;
+$search-input-width: 220px;
$location-badge-active-bg: #4f91f8;
$location-icon-color: #e7e9ed;
-$location-icon-active-color: #807e7e;
/*
* Notes
*/
$notes-light-color: #8e8e8e;
-$notes-action-color: #c3c3c3;
$notes-role-color: #8e8e8e;
-$notes-role-border-color: #e4e4e4;
-
$note-disabled-comment-color: #b2b2b2;
-$note-form-border-color: #e5e5e5;
-$note-toolbar-color: #959494;
+$note-targe3-outside: #fffff0;
+$note-targe3-inside: #ffffd3;
+$note-line2-border: #ddd;
+
-$zen-control-hover-color: #111;
+/*
+* Zen
+*/
+$zen-control-color: #555;
-$calendar-header-color: #b8b8b8;
+/*
+* Calendar
+*/
$calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1);
-$calendar-unselectable-bg: $gray-light;
+$calendar-user-contrib-text: #959494;
/*
* Cycle Analytics
*/
$cycle-analytics-box-padding: 30px;
$cycle-analytics-box-text-color: #8c8c8c;
+$cycle-analytics-big-font: 19px;
+$cycle-analytics-dark-text: $gl-title-color;
+$cycle-analytics-light-gray: #bfbfbf;
+$cycle-analytics-dismiss-icon-color: #b2b2b2;
/*
- * Personal Access Tokens
- */
-$personal-access-tokens-disabled-label-color: #bbb;
+* CI
+*/
+$ci-skipped-color: #888;
+
+/*
+* Boards
+*/
+$issue-boards-font-size: 14px;
+$issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
+
+/*
+* Avatar
+*/
+$avatar_radius: 50%;
+$avatar-border: rgba(0, 0, 0, .1);
+$gl-avatar-size: 40px;
+
+/*
+* Builds
+*/
+$builds-trace-bg: #111;
+
+/*
+* Callout
+*/
+$callout-danger-bg: #fdf7f7;
+$callout-danger-border: #eed3d7;
+$callout-danger-color: #b94a48;
+$callout-warning-bg: #faf8f0;
+$callout-warning-border: #faebcc;
+$callout-warning-color: #8a6d3b;
+$callout-info-bg: #f4f8fa;
+$callout-info-border: #bce8f1;
+$callout-info-color: #34789a;
+$callout-success-bg: #dff0d8;
+$callout-success-border: #5ca64d;
+$callout-success-color: #3c763d;
+
+/*
+* Commit Page
+*/
+$commit-committer-color: #999;
+$commit-max-width-marker-color: rgba(0, 0, 0, 0.0);
+$commit-message-text-area-bg: rgba(0, 0, 0, 0.0);
+
+/*
+* Common
+*/
+$common-gray: $gl-gray;
+$common-gray-light: #bbb;
+$common-gray-dark: #444;
+$common-red: $gl-text-red;
+$common-green: $gl-text-green;
+
+/*
+* Editor
+*/
+$editor-cancel-color: #b94a48;
+
+/*
+* Events
+*/
+$events-pre-color: #777;
+$events-note-icon-color: #777;
+$events-body-border: #ddd;
+
+/*
+* Files
+*/
+$file-image-bg: #eee;
+$blob-bg: #eee;
+$blame-border: #eee;
+$blame-line-numbers-border: #ddd;
+$logs-bg: #eee;
+$logs-li-color: #888;
+$logs-p-color: #333;
-$ci-output-bg: #1d1f21;
-$ci-text-color: #c5c8c6;
+/*
+* Forms
+*/
+$input-danger-bg: #f2dede;
+$input-danger-border: #d66;
+$input-group-addon-bg: #f7f8fa;
+$gl-field-focus-shadow: rgba(0, 0, 0, 0.075);
+$gl-field-focus-shadow-error: rgba(210, 40, 82, 0.6);
-$issue-boards-font-size: 15px;
+/*
+* Help
+*/
+$document-index-color: #888;
+$help-shortcut-color: #999;
+$help-shortcut-mapping-color: #555;
+$help-shortcut-header-color: #333;
+
+/*
+* Issues
+*/
+$issues-today-bg: #f3fff2;
+$issues-today-border: #e1e8d5;
+
+/*
+* jQuery UI
+*/
+$jq-ui-border: #ddd;
+$jq-ui-default-color: #777;
+
+/*
+* Label
+*/
+$label-gray-bg: #f8fafc;
+$label-inverse-bg: #333;
+$label-remove-border: rgba(0, 0, 0, .1);
+$label-border-radius: 100px;
+
+/*
+* Lint
+*/
+$lint-incorrect-color: red;
+$lint-correct-color: #47a447;
+
+/*
+* Login
+*/
+$login-brand-holder-color: #888;
+$login-devise-error-color: #a00;
+
+/*
+* Nav
+*/
+$nav-link-gray: #959494;
+$nav-badge-bg: #eee;
+$nav-toggle-gray: #666;
+
+/*
+* Notify
+*/
+$notify-details: #777;
+$notify-footer: #777;
+$notify-new-file: #090;
+$notify-deleted-file: #b00;
+
+/*
+* Projects
+*/
+$project-option-descr-color: #54565b;
+$project-breadcrumb-color: #999;
+$project-private-forks-notice-odd: #2aa056;
+$project-network-controls-color: #888;
+
+/*
+* Runners
+*/
+$runner-state-shared-bg: #32b186;
+$runner-state-specific-bg: #3498db;
+$runner-status-online-color: green;
+$runner-status-offline-color: gray;
+$runner-status-paused-color: red;
+
+/*
+Stat Graph
+*/
+$stat-graph-common-bg: #f3f3f3;
+$stat-graph-area-fill: #1db34f;
+$stat-graph-axis-fill: #aaa;
+$stat-graph-orange-fill: #f17f49;
+$stat-graph-selection-fill: #333;
+$stat-graph-selection-stroke: #333;
+
+/*
+* Selects
+*/
+$select2-drop-shadow1: rgba(76, 86, 103, 0.247059);
+$select2-drop-shadow2: rgba(31, 37, 50, 0.317647);
+
+
+/*
+* Todo
+*/
+$todo-alert-blue: #428bca;
+$todo-body-pre-color: #777;
+$todo-body-border: #ddd;
+
+/*
+* Typography
+*/
+$kdb-bg: #fcfcfc;
+$kdb-color: #555;
+$kdb-border: #ccc;
+$kdb-border-bottom: #bbb;
+$kdb-shadow: #bbb;
+$body-text-shadow: rgba(255,255,255,0.01);
+
+/*
+* UI Dev Kit
+*/
+$ui-dev-kit-example-color: #bbb;
+$ui-dev-kit-example-border: #ddd;
+
+/*
+Pipeline Graph
+*/
+$stage-hover-bg: #eaf3fc;
+$stage-hover-border: #d1e7fc;
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
new file mode 100644
index 00000000000..f9c850ebdc8
--- /dev/null
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -0,0 +1,58 @@
+.info-well {
+ background: $gray-light;
+ color: $gl-gray;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+
+ .well-segment {
+ padding: $gl-padding;
+
+ &:not(:last-of-type) {
+ border-bottom: 1px solid $well-inner-border;
+ }
+
+ &.branch-info {
+ .monospace,
+ .commit-info {
+ margin-left: 4px;
+ }
+ }
+ }
+
+ .icon-container {
+ display: inline-block;
+ margin-right: 8px;
+
+ svg {
+ position: relative;
+ top: 2px;
+ height: 16px;
+ width: 16px;
+ }
+
+ &.commit-icon {
+ svg {
+ path {
+ fill: $gl-text-color;
+ }
+ }
+ }
+ }
+
+ .label.label-gray {
+ background-color: $well-expand-item;
+ }
+}
+
+.light-well {
+ background-color: $gray-light;
+ padding: 15px;
+}
+
+.well-centered {
+ h1 {
+ font-weight: normal;
+ text-align: center;
+ font-size: 48px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index ff02ebdd34c..84b639fabf5 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -1,6 +1,6 @@
.zen-backdrop {
&.fullscreen {
- background-color: white;
+ background-color: $white-light;
position: fixed;
top: 0;
bottom: 0;
@@ -12,7 +12,7 @@
border: none;
box-shadow: none;
border-radius: 0;
- color: #000;
+ color: $black;
font-size: 20px;
line-height: 26px;
padding: 30px;
@@ -34,13 +34,13 @@
.zen-control {
padding: 0;
- color: #555;
+ color: $zen-control-color;
background: none;
border: 0;
}
.zen-control-full {
- color: $note-toolbar-color;
+ color: $gl-gray-light;
&:hover {
color: $gl-link-color;
@@ -57,6 +57,6 @@
font-size: 36px;
&:hover {
- color: $zen-control-hover-color;
+ color: $black;
}
}
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index d22d9b01495..cb923166b25 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -1,27 +1,109 @@
/* https://github.com/MozMorris/tomorrow-pygments */
+
+/*
+* Dark syntax colors
+*/
+$dark-new-bg: rgba(51, 255, 51, 0.1);
+$dark-new-idiff: rgba(51, 255, 51, 0.2);
+$dark-old-bg: rgba(255, 51, 51, 0.2);
+$dark-old-idiff: rgba(255, 51, 51, 0.25);
+$dark-border: #808080;
+$dark-code-border: #666;
+$dark-main-bg: #1d1f21;
+$dark-main-color: #1d1f21;
+$dark-line-color: #c5c8c6;
+$dark-line-num-color: rgba(255, 255, 255, 0.3);
+$dark-diff-not-empty-bg: #557;
+$dark-highlight-bg: #ffe792;
+$dark-highlight-color: $black;
+$dark-pre-hll-bg: #373b41;
+$dark-hll-bg: #373b41;
+$dark-c: #969896;
+$dark-err: #c66;
+$dark-k: #b294bb;
+$dark-l: #de935f;
+$dark-n: #c5c8c6;
+$dark-o: #8abeb7;
+$dark-p: #c5c8c6;
+$dark-cm: #969896;
+$dark-cp: #969896;
+$dark-c1: #969896;
+$dark-cs: #969896;
+$dark-gd: #c66;
+$dark-gh: #c5c8c6;
+$dark-gi: #b5bd68;
+$dark-gp: #969896;
+$dark-gu: #8abeb7;
+$dark-kc: #b294bb;
+$dark-kd: #b294bb;
+$dark-kn: #8abeb7;
+$dark-kp: #b294bb;
+$dark-kr: #b294bb;
+$dark-kt: #f0c674;
+$dark-ld: #b5bd68;
+$dark-m: #de935f;
+$dark-s: #b5bd68;
+$dark-na: #81a2be;
+$dark-nb: #c5c8c6;
+$dark-nc: #f0c674;
+$dark-no: #c66;
+$dark-nd: #8abeb7;
+$dark-ni: #c5c8c6;
+$dark-ne: #c66;
+$dark-nf: #81a2be;
+$dark-nl: #c5c8c6;
+$dark-nn: #f0c674;
+$dark-nx: #81a2be;
+$dark-py: #c5c8c6;
+$dark-nt: #8abeb7;
+$dark-nv: #c66;
+$dark-ow: #8abeb7;
+$dark-w: #c5c8c6;
+$dark-mf: #de935f;
+$dark-mh: #de935f;
+$dark-mi: #de935f;
+$dark-mo: #de935f;
+$dark-sb: #b5bd68;
+$dark-sc: #c5c8c6;
+$dark-sd: #969896;
+$dark-s2: #b5bd68;
+$dark-se: #de935f;
+$dark-sh: #b5bd68;
+$dark-si: #de935f;
+$dark-sx: #b5bd68;
+$dark-sr: #b5bd68;
+$dark-s1: #b5bd68;
+$dark-ss: #b5bd68;
+$dark-bp: #c5c8c6;
+$dark-vc: #c66;
+$dark-vg: #c66;
+$dark-vi: #c66;
+$dark-il: #de935f;
+
.code.dark {
// Line numbers
.line-numbers,
.diff-line-num {
- background-color: #1d1f21;
+ background-color: $dark-main-bg;
}
.diff-line-num,
.diff-line-num a {
- color: rgba(255, 255, 255, 0.3);
+ color: $dark-main-color;
+ color: $dark-line-num-color;
}
// Code itself
pre.code,
.diff-line-num {
- border-color: #666;
+ border-color: $dark-code-border;
}
&,
pre.code,
.line_holder .line_content {
- background-color: #1d1f21;
- color: #c5c8c6;
+ background-color: $dark-main-bg;
+ color: $dark-line-color;
}
// Diff line
@@ -32,18 +114,18 @@
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
- background-color: #557;
- border-color: darken(#557, 15%);
+ background-color: $dark-diff-not-empty-bg;
+ border-color: darken($dark-diff-not-empty-bg, 15%);
}
.diff-line-num.new,
.line_content.new {
- @include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080);
+ @include diff_background($dark-new-bg, $dark-new-idiff, $dark-border);
}
.diff-line-num.old,
.line_content.old {
- @include diff_background(rgba(255, 51, 51, 0.2), rgba(255, 51, 51, 0.25), #808080);
+ @include diff_background($dark-old-bg, $dark-old-idiff, $dark-border);
}
.line_content.match {
@@ -53,77 +135,77 @@
// highlight line via anchor
pre .hll {
- background-color: #557 !important;
+ background-color: $dark-pre-hll-bg !important;
}
// Search result highlight
span.highlight_word {
- background-color: #ffe792 !important;
- color: #000 !important;
+ background-color: $dark-highlight-bg !important;
+ color: $dark-highlight-color !important;
}
- .hll { background-color: #373b41; }
- .c { color: #969896; } /* Comment */
- .err { color: #c66; } /* Error */
- .k { color: #b294bb; } /* Keyword */
- .l { color: #de935f; } /* Literal */
- .n { color: #c5c8c6; } /* Name */
- .o { color: #8abeb7; } /* Operator */
- .p { color: #c5c8c6; } /* Punctuation */
- .cm { color: #969896; } /* Comment.Multiline */
- .cp { color: #969896; } /* Comment.Preproc */
- .c1 { color: #969896; } /* Comment.Single */
- .cs { color: #969896; } /* Comment.Special */
- .gd { color: #c66; } /* Generic.Deleted */
+ .hll { background-color: $dark-hll-bg; }
+ .c { color: $dark-c; } /* Comment */
+ .err { color: $dark-err; } /* Error */
+ .k { color: $dark-k; } /* Keyword */
+ .l { color: $dark-l; } /* Literal */
+ .n { color: $dark-n; } /* Name */
+ .o { color: $dark-o; } /* Operator */
+ .p { color: $dark-p; } /* Punctuation */
+ .cm { color: $dark-cm; } /* Comment.Multiline */
+ .cp { color: $dark-cp; } /* Comment.Preproc */
+ .c1 { color: $dark-c1; } /* Comment.Single */
+ .cs { color: $dark-cs; } /* Comment.Special */
+ .gd { color: $dark-gd; } /* Generic.Deleted */
.ge { font-style: italic; } /* Generic.Emph */
- .gh { color: #c5c8c6; font-weight: bold; } /* Generic.Heading */
- .gi { color: #b5bd68; } /* Generic.Inserted */
- .gp { color: #969896; font-weight: bold; } /* Generic.Prompt */
+ .gh { color: $dark-gh; font-weight: bold; } /* Generic.Heading */
+ .gi { color: $dark-gi; } /* Generic.Inserted */
+ .gp { color: $dark-gp; font-weight: bold; } /* Generic.Prompt */
.gs { font-weight: bold; } /* Generic.Strong */
- .gu { color: #8abeb7; font-weight: bold; } /* Generic.Subheading */
- .kc { color: #b294bb; } /* Keyword.Constant */
- .kd { color: #b294bb; } /* Keyword.Declaration */
- .kn { color: #8abeb7; } /* Keyword.Namespace */
- .kp { color: #b294bb; } /* Keyword.Pseudo */
- .kr { color: #b294bb; } /* Keyword.Reserved */
- .kt { color: #f0c674; } /* Keyword.Type */
- .ld { color: #b5bd68; } /* Literal.Date */
- .m { color: #de935f; } /* Literal.Number */
- .s { color: #b5bd68; } /* Literal.String */
- .na { color: #81a2be; } /* Name.Attribute */
- .nb { color: #c5c8c6; } /* Name.Builtin */
- .nc { color: #f0c674; } /* Name.Class */
- .no { color: #c66; } /* Name.Constant */
- .nd { color: #8abeb7; } /* Name.Decorator */
- .ni { color: #c5c8c6; } /* Name.Entity */
- .ne { color: #c66; } /* Name.Exception */
- .nf { color: #81a2be; } /* Name.Function */
- .nl { color: #c5c8c6; } /* Name.Label */
- .nn { color: #f0c674; } /* Name.Namespace */
- .nx { color: #81a2be; } /* Name.Other */
- .py { color: #c5c8c6; } /* Name.Property */
- .nt { color: #8abeb7; } /* Name.Tag */
- .nv { color: #c66; } /* Name.Variable */
- .ow { color: #8abeb7; } /* Operator.Word */
- .w { color: #c5c8c6; } /* Text.Whitespace */
- .mf { color: #de935f; } /* Literal.Number.Float */
- .mh { color: #de935f; } /* Literal.Number.Hex */
- .mi { color: #de935f; } /* Literal.Number.Integer */
- .mo { color: #de935f; } /* Literal.Number.Oct */
- .sb { color: #b5bd68; } /* Literal.String.Backtick */
- .sc { color: #c5c8c6; } /* Literal.String.Char */
- .sd { color: #969896; } /* Literal.String.Doc */
- .s2 { color: #b5bd68; } /* Literal.String.Double */
- .se { color: #de935f; } /* Literal.String.Escape */
- .sh { color: #b5bd68; } /* Literal.String.Heredoc */
- .si { color: #de935f; } /* Literal.String.Interpol */
- .sx { color: #b5bd68; } /* Literal.String.Other */
- .sr { color: #b5bd68; } /* Literal.String.Regex */
- .s1 { color: #b5bd68; } /* Literal.String.Single */
- .ss { color: #b5bd68; } /* Literal.String.Symbol */
- .bp { color: #c5c8c6; } /* Name.Builtin.Pseudo */
- .vc { color: #c66; } /* Name.Variable.Class */
- .vg { color: #c66; } /* Name.Variable.Global */
- .vi { color: #c66; } /* Name.Variable.Instance */
- .il { color: #de935f; } /* Literal.Number.Integer.Long */
+ .gu { color: $dark-gu; font-weight: bold; } /* Generic.Subheading */
+ .kc { color: $dark-kc; } /* Keyword.Constant */
+ .kd { color: $dark-kd; } /* Keyword.Declaration */
+ .kn { color: $dark-kn; } /* Keyword.Namespace */
+ .kp { color: $dark-kp; } /* Keyword.Pseudo */
+ .kr { color: $dark-kr; } /* Keyword.Reserved */
+ .kt { color: $dark-kt; } /* Keyword.Type */
+ .ld { color: $dark-ld; } /* Literal.Date */
+ .m { color: $dark-m; } /* Literal.Number */
+ .s { color: $dark-s; } /* Literal.String */
+ .na { color: $dark-na; } /* Name.Attribute */
+ .nb { color: $dark-nb; } /* Name.Builtin */
+ .nc { color: $dark-nc; } /* Name.Class */
+ .no { color: $dark-no; } /* Name.Constant */
+ .nd { color: $dark-nd; } /* Name.Decorator */
+ .ni { color: $dark-ni; } /* Name.Entity */
+ .ne { color: $dark-ne; } /* Name.Exception */
+ .nf { color: $dark-nf; } /* Name.Function */
+ .nl { color: $dark-nl; } /* Name.Label */
+ .nn { color: $dark-nn; } /* Name.Namespace */
+ .nx { color: $dark-nx; } /* Name.Other */
+ .py { color: $dark-py; } /* Name.Property */
+ .nt { color: $dark-nt; } /* Name.Tag */
+ .nv { color: $dark-nv; } /* Name.Variable */
+ .ow { color: $dark-ow; } /* Operator.Word */
+ .w { color: $dark-w; } /* Text.Whitespace */
+ .mf { color: $dark-mf; } /* Literal.Number.Float */
+ .mh { color: $dark-mh; } /* Literal.Number.Hex */
+ .mi { color: $dark-mi; } /* Literal.Number.Integer */
+ .mo { color: $dark-mo; } /* Literal.Number.Oct */
+ .sb { color: $dark-sb; } /* Literal.String.Backtick */
+ .sc { color: $dark-sc; } /* Literal.String.Char */
+ .sd { color: $dark-sd; } /* Literal.String.Doc */
+ .s2 { color: $dark-s2; } /* Literal.String.Double */
+ .se { color: $dark-se; } /* Literal.String.Escape */
+ .sh { color: $dark-sh; } /* Literal.String.Heredoc */
+ .si { color: $dark-si; } /* Literal.String.Interpol */
+ .sx { color: $dark-sx; } /* Literal.String.Other */
+ .sr { color: $dark-sr; } /* Literal.String.Regex */
+ .s1 { color: $dark-s1; } /* Literal.String.Single */
+ .ss { color: $dark-ss; } /* Literal.String.Symbol */
+ .bp { color: $dark-bp; } /* Name.Builtin.Pseudo */
+ .vc { color: $dark-vc; } /* Name.Variable.Class */
+ .vg { color: $dark-vg; } /* Name.Variable.Global */
+ .vi { color: $dark-vi; } /* Name.Variable.Instance */
+ .il { color: $dark-il; } /* Literal.Number.Integer.Long */
}
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index db8da8aab10..d8510baad8a 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -1,27 +1,108 @@
/* https://github.com/richleland/pygments-css/blob/master/monokai.css */
+
+/*
+* Monokai Colors
+*/
+$monokai-bg: #272822;
+$monokai-border: #555;
+$monokai-text-color: #f8f8f2;
+$monokai-line-num-color: rgba(255, 255, 255, 0.3);
+$monokai-line-empty-bg: #49483e;
+$monokai-line-empty-border: darken($monokai-line-empty-bg, 15%);
+$monokai-diff-border: #808080;
+$monokai-highlight-bg: #ffe792;
+
+$monokai-new-bg: rgba(166, 226, 46, 0.1);
+$monokai-new-idiff: rgba(166, 226, 46, 0.15);
+
+$monokai-old-bg: rgba(254, 147, 140, 0.15);
+$monokai-old-idiff: rgba(254, 147, 140, 0.2);
+
+$monokai-hll: #49483e;
+$monokai-c: #75715e;
+$monokai-err-color: #960050;
+$monokai-err-bg: #1e0010;
+$monokai-k: #66d9ef;
+$monokai-l: #ae81ff;
+$monokai-n: #f8f8f2;
+$monokai-o: #f92672;
+$monokai-p: #f8f8f2;
+$monokai-cm: #75715e;
+$monokai-cp: #75715e;
+$monokai-c1: #75715e;
+$monokai-cs: #75715e;
+$monokai-kc: #66d9ef;
+$monokai-kd: #66d9ef;
+$monokai-kn: #f92672;
+$monokai-kp: #66d9ef;
+$monokai-kr: #66d9ef;
+$monokai-kt: #66d9ef;
+$monokai-ld: #e6db74;
+$monokai-m: #ae81ff;
+$monokai-s: #e6db74;
+$monokai-na: #a6e22e;
+$monokai-nb: #f8f8f2;
+$monokai-nc: #a6e22e;
+$monokai-no: #66d9ef;
+$monokai-nd: #a6e22e;
+$monokai-ni: #f8f8f2;
+$monokai-ne: #a6e22e;
+$monokai-nf: #a6e22e;
+$monokai-nl: #f8f8f2;
+$monokai-nn: #f8f8f2;
+$monokai-nx: #a6e22e;
+$monokai-py: #f8f8f2;
+$monokai-nt: #f92672;
+$monokai-nv: #f8f8f2;
+$monokai-ow: #f92672;
+$monokai-w: #f8f8f2;
+$monokai-mf: #ae81ff;
+$monokai-mh: #ae81ff;
+$monokai-mi: #ae81ff;
+$monokai-mo: #ae81ff;
+$monokai-sb: #e6db74;
+$monokai-sc: #e6db74;
+$monokai-sd: #e6db74;
+$monokai-s2: #e6db74;
+$monokai-se: #ae81ff;
+$monokai-sh: #e6db74;
+$monokai-si: #e6db74;
+$monokai-sx: #e6db74;
+$monokai-sr: #e6db74;
+$monokai-s1: #e6db74;
+$monokai-ss: #e6db74;
+$monokai-bp: #f8f8f2;
+$monokai-vc: #f8f8f2;
+$monokai-vg: #f8f8f2;
+$monokai-vi: #f8f8f2;
+$monokai-il: #ae81ff;
+$monokai-gu: #75715e;
+$monokai-gd: #f92672;
+$monokai-gi: #a6e22e;
+
.code.monokai {
// Line numbers
.line-numbers,
.diff-line-num {
- background-color: #272822;
+ background-color: $monokai-bg;
}
.diff-line-num,
.diff-line-num a {
- color: rgba(255, 255, 255, 0.3);
+ color: $monokai-line-num-color;
}
// Code itself
pre.code,
.diff-line-num {
- border-color: #555;
+ border-color: $monokai-border;
}
&,
pre.code,
.line_holder .line_content {
- background-color: #272822;
- color: #f8f8f2;
+ background-color: $monokai-bg;
+ color: $monokai-text-color;
}
// Diff line
@@ -32,18 +113,18 @@
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
- background-color: #49483e;
- border-color: darken(#49483e, 15%);
+ background-color: $monokai-line-empty-bg;
+ border-color: $monokai-line-empty-border;
}
.diff-line-num.new,
.line_content.new {
- @include diff_background(rgba(166, 226, 46, 0.1), rgba(166, 226, 46, 0.15), #808080);
+ @include diff_background($monokai-new-bg, $monokai-new-idiff, $monokai-diff-border);
}
.diff-line-num.old,
.line_content.old {
- @include diff_background(rgba(254, 147, 140, 0.15), rgba(254, 147, 140, 0.2), #808080);
+ @include diff_background($monokai-old-bg, $monokai-old-idiff, $monokai-diff-border);
}
.line_content.match {
@@ -53,75 +134,75 @@
// highlight line via anchor
pre .hll {
- background-color: #49483e !important;
+ background-color: $monokai-hll !important;
}
// Search result highlight
span.highlight_word {
- background-color: #ffe792 !important;
- color: #000 !important;
+ background-color: $monokai-highlight-bg !important;
+ color: $black !important;
}
- .hll { background-color: #49483e; }
- .c { color: #75715e; } /* Comment */
- .err { color: #960050; background-color: #1e0010; } /* Error */
- .k { color: #66d9ef; } /* Keyword */
- .l { color: #ae81ff; } /* Literal */
- .n { color: #f8f8f2; } /* Name */
- .o { color: #f92672; } /* Operator */
- .p { color: #f8f8f2; } /* Punctuation */
- .cm { color: #75715e; } /* Comment.Multiline */
- .cp { color: #75715e; } /* Comment.Preproc */
- .c1 { color: #75715e; } /* Comment.Single */
- .cs { color: #75715e; } /* Comment.Special */
+ .hll { background-color: $monokai-hll; }
+ .c { color: $monokai-c; } /* Comment */
+ .err { color: $monokai-err-color; background-color: $monokai-err-bg; } /* Error */
+ .k { color: $monokai-k; } /* Keyword */
+ .l { color: $monokai-l; } /* Literal */
+ .n { color: $monokai-n; } /* Name */
+ .o { color: $monokai-o; } /* Operator */
+ .p { color: $monokai-p; } /* Punctuation */
+ .cm { color: $monokai-cm; } /* Comment.Multiline */
+ .cp { color: $monokai-cp; } /* Comment.Preproc */
+ .c1 { color: $monokai-c1; } /* Comment.Single */
+ .cs { color: $monokai-cs; } /* Comment.Special */
.ge { font-style: italic; } /* Generic.Emph */
.gs { font-weight: bold; } /* Generic.Strong */
- .kc { color: #66d9ef; } /* Keyword.Constant */
- .kd { color: #66d9ef; } /* Keyword.Declaration */
- .kn { color: #f92672; } /* Keyword.Namespace */
- .kp { color: #66d9ef; } /* Keyword.Pseudo */
- .kr { color: #66d9ef; } /* Keyword.Reserved */
- .kt { color: #66d9ef; } /* Keyword.Type */
- .ld { color: #e6db74; } /* Literal.Date */
- .m { color: #ae81ff; } /* Literal.Number */
- .s { color: #e6db74; } /* Literal.String */
- .na { color: #a6e22e; } /* Name.Attribute */
- .nb { color: #f8f8f2; } /* Name.Builtin */
- .nc { color: #a6e22e; } /* Name.Class */
- .no { color: #66d9ef; } /* Name.Constant */
- .nd { color: #a6e22e; } /* Name.Decorator */
- .ni { color: #f8f8f2; } /* Name.Entity */
- .ne { color: #a6e22e; } /* Name.Exception */
- .nf { color: #a6e22e; } /* Name.Function */
- .nl { color: #f8f8f2; } /* Name.Label */
- .nn { color: #f8f8f2; } /* Name.Namespace */
- .nx { color: #a6e22e; } /* Name.Other */
- .py { color: #f8f8f2; } /* Name.Property */
- .nt { color: #f92672; } /* Name.Tag */
- .nv { color: #f8f8f2; } /* Name.Variable */
- .ow { color: #f92672; } /* Operator.Word */
- .w { color: #f8f8f2; } /* Text.Whitespace */
- .mf { color: #ae81ff; } /* Literal.Number.Float */
- .mh { color: #ae81ff; } /* Literal.Number.Hex */
- .mi { color: #ae81ff; } /* Literal.Number.Integer */
- .mo { color: #ae81ff; } /* Literal.Number.Oct */
- .sb { color: #e6db74; } /* Literal.String.Backtick */
- .sc { color: #e6db74; } /* Literal.String.Char */
- .sd { color: #e6db74; } /* Literal.String.Doc */
- .s2 { color: #e6db74; } /* Literal.String.Double */
- .se { color: #ae81ff; } /* Literal.String.Escape */
- .sh { color: #e6db74; } /* Literal.String.Heredoc */
- .si { color: #e6db74; } /* Literal.String.Interpol */
- .sx { color: #e6db74; } /* Literal.String.Other */
- .sr { color: #e6db74; } /* Literal.String.Regex */
- .s1 { color: #e6db74; } /* Literal.String.Single */
- .ss { color: #e6db74; } /* Literal.String.Symbol */
- .bp { color: #f8f8f2; } /* Name.Builtin.Pseudo */
- .vc { color: #f8f8f2; } /* Name.Variable.Class */
- .vg { color: #f8f8f2; } /* Name.Variable.Global */
- .vi { color: #f8f8f2; } /* Name.Variable.Instance */
- .il { color: #ae81ff; } /* Literal.Number.Integer.Long */
- .gu { color: #75715e; } /* Generic.Subheading & Diff Unified/Comment? */
- .gd { color: #f92672; } /* Generic.Deleted & Diff Deleted */
- .gi { color: #a6e22e; } /* Generic.Inserted & Diff Inserted */
+ .kc { color: $monokai-kc; } /* Keyword.Constant */
+ .kd { color: $monokai-kd; } /* Keyword.Declaration */
+ .kn { color: $monokai-kn; } /* Keyword.Namespace */
+ .kp { color: $monokai-kp; } /* Keyword.Pseudo */
+ .kr { color: $monokai-kr; } /* Keyword.Reserved */
+ .kt { color: $monokai-kt; } /* Keyword.Type */
+ .ld { color: $monokai-ld; } /* Literal.Date */
+ .m { color: $monokai-m; } /* Literal.Number */
+ .s { color: $monokai-s; } /* Literal.String */
+ .na { color: $monokai-na; } /* Name.Attribute */
+ .nb { color: $monokai-nb; } /* Name.Builtin */
+ .nc { color: $monokai-nc; } /* Name.Class */
+ .no { color: $monokai-no; } /* Name.Constant */
+ .nd { color: $monokai-nd; } /* Name.Decorator */
+ .ni { color: $monokai-ni; } /* Name.Entity */
+ .ne { color: $monokai-ne; } /* Name.Exception */
+ .nf { color: $monokai-nf; } /* Name.Function */
+ .nl { color: $monokai-nl; } /* Name.Label */
+ .nn { color: $monokai-nn; } /* Name.Namespace */
+ .nx { color: $monokai-nx; } /* Name.Other */
+ .py { color: $monokai-py; } /* Name.Property */
+ .nt { color: $monokai-nt; } /* Name.Tag */
+ .nv { color: $monokai-nv; } /* Name.Variable */
+ .ow { color: $monokai-ow; } /* Operator.Word */
+ .w { color: $monokai-w; } /* Text.Whitespace */
+ .mf { color: $monokai-mf; } /* Literal.Number.Float */
+ .mh { color: $monokai-mh; } /* Literal.Number.Hex */
+ .mi { color: $monokai-mi; } /* Literal.Number.Integer */
+ .mo { color: $monokai-mo; } /* Literal.Number.Oct */
+ .sb { color: $monokai-sb; } /* Literal.String.Backtick */
+ .sc { color: $monokai-sc; } /* Literal.String.Char */
+ .sd { color: $monokai-sd; } /* Literal.String.Doc */
+ .s2 { color: $monokai-s2; } /* Literal.String.Double */
+ .se { color: $monokai-se; } /* Literal.String.Escape */
+ .sh { color: $monokai-sh; } /* Literal.String.Heredoc */
+ .si { color: $monokai-si; } /* Literal.String.Interpol */
+ .sx { color: $monokai-sx; } /* Literal.String.Other */
+ .sr { color: $monokai-sr; } /* Literal.String.Regex */
+ .s1 { color: $monokai-s1; } /* Literal.String.Single */
+ .ss { color: $monokai-ss; } /* Literal.String.Symbol */
+ .bp { color: $monokai-bp; } /* Name.Builtin.Pseudo */
+ .vc { color: $monokai-vc; } /* Name.Variable.Class */
+ .vg { color: $monokai-vg; } /* Name.Variable.Global */
+ .vi { color: $monokai-vi; } /* Name.Variable.Instance */
+ .il { color: $monokai-il; } /* Literal.Number.Integer.Long */
+ .gu { color: $monokai-gu; } /* Generic.Subheading & Diff Unified/Comment? */
+ .gd { color: $monokai-gd; } /* Generic.Deleted & Diff Deleted */
+ .gi { color: $monokai-gi; } /* Generic.Inserted & Diff Inserted */
}
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index a87333146de..874aecb5e16 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -1,27 +1,112 @@
/* https://gist.github.com/qguv/7936275 */
+
+/*
+* Solarized dark colors
+*/
+$solarized-dark-new-bg: rgba(133, 153, 0, 0.15);
+$solarized-dark-new-idiff: rgba(133, 153, 0, 0.25);
+$solarized-dark-old-bg: rgba(220, 50, 47, 0.3);
+$solarized-dark-old-idiff: rgba(220, 50, 47, 0.25);
+$solarized-dark-border: #113b46;
+$solarized-dark-pre-bg: #002b36;
+$solarized-dark-pre-color: #93a1a1;
+$solarized-dark-pre-border: #113b46;
+$solarized-dark-line-bg: #002b36;
+$solarized-dark-line-color: rgba(255, 255, 255, 0.3);
+$solarized-dark-highlight: #094554;
+$solarized-dark-hll-bg: #174652;
+$solarized-dark-c: #586e75;
+$solarized-dark-err: #93a1a1;
+$solarized-dark-g: #93a1a1;
+$solarized-dark-k: #859900;
+$solarized-dark-l: #93a1a1;
+$solarized-dark-n: #93a1a1;
+$solarized-dark-o: #859900;
+$solarized-dark-x: #cb4b16;
+$solarized-dark-p: #93a1a1;
+$solarized-dark-cm: #586e75;
+$solarized-dark-cp: #859900;
+$solarized-dark-c1: #586e75;
+$solarized-dark-cs: #859900;
+$solarized-dark-gd: #2aa198;
+$solarized-dark-ge: #93a1a1;
+$solarized-dark-gr: #dc322f;
+$solarized-dark-gh: #cb4b16;
+$solarized-dark-gi: #859900;
+$solarized-dark-go: #93a1a1;
+$solarized-dark-gp: #93a1a1;
+$solarized-dark-gs: #93a1a1;
+$solarized-dark-gu: #cb4b16;
+$solarized-dark-gt: #93a1a1;
+$solarized-dark-kc: #cb4b16;
+$solarized-dark-kd: #268bd2;
+$solarized-dark-kn: #859900;
+$solarized-dark-kp: #859900;
+$solarized-dark-kr: #268bd2;
+$solarized-dark-kt: #dc322f;
+$solarized-dark-ld: #93a1a1;
+$solarized-dark-m: #2aa198;
+$solarized-dark-s: #2aa198;
+$solarized-dark-na: #93a1a1;
+$solarized-dark-nb: #b58900;
+$solarized-dark-nc: #268bd2;
+$solarized-dark-no: #cb4b16;
+$solarized-dark-nd: #268bd2;
+$solarized-dark-ni: #cb4b16;
+$solarized-dark-ne: #cb4b16;
+$solarized-dark-nf: #268bd2;
+$solarized-dark-nl: #93a1a1;
+$solarized-dark-nn: #93a1a1;
+$solarized-dark-nx: #93a1a1;
+$solarized-dark-py: #93a1a1;
+$solarized-dark-nt: #268bd2;
+$solarized-dark-nv: #268bd2;
+$solarized-dark-ow: #859900;
+$solarized-dark-w: #93a1a1;
+$solarized-dark-mf: #2aa198;
+$solarized-dark-mh: #2aa198;
+$solarized-dark-mi: #2aa198;
+$solarized-dark-mo: #2aa198;
+$solarized-dark-sb: #586e75;
+$solarized-dark-sc: #2aa198;
+$solarized-dark-sd: #93a1a1;
+$solarized-dark-s2: #2aa198;
+$solarized-dark-se: #cb4b16;
+$solarized-dark-sh: #93a1a1;
+$solarized-dark-si: #2aa198;
+$solarized-dark-sx: #2aa198;
+$solarized-dark-sr: #dc322f;
+$solarized-dark-s1: #2aa198;
+$solarized-dark-ss: #2aa198;
+$solarized-dark-bp: #268bd2;
+$solarized-dark-vc: #268bd2;
+$solarized-dark-vg: #268bd2;
+$solarized-dark-vi: #268bd2;
+$solarized-dark-il: #2aa198;
+
.code.solarized-dark {
// Line numbers
.line-numbers,
.diff-line-num {
- background-color: #002b36;
+ background-color: $solarized-dark-line-bg;
}
.diff-line-num,
.diff-line-num a {
- color: rgba(255, 255, 255, 0.3);
+ color: $solarized-dark-line-color;
}
// Code itself
pre.code,
.diff-line-num {
- border-color: #113b46;
+ border-color: $solarized-dark-pre-border;
}
&,
pre.code,
.line_holder .line_content {
- background-color: #002b36;
- color: #93a1a1;
+ background-color: $solarized-dark-pre-bg;
+ color: $solarized-dark-pre-color;
}
// Diff line
@@ -32,18 +117,18 @@
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
- background-color: #174652;
- border-color: darken(#174652, 15%);
+ background-color: $solarized-dark-hll-bg;
+ border-color: darken($solarized-dark-hll-bg, 15%);
}
.diff-line-num.new,
.line_content.new {
- @include diff_background(rgba(133, 153, 0, 0.15), rgba(133, 153, 0, 0.25), #113b46);
+ @include diff_background($solarized-dark-new-bg, $solarized-dark-new-idiff, $solarized-dark-border);
}
.diff-line-num.old,
.line_content.old {
- @include diff_background(rgba(220, 50, 47, 0.3), rgba(220, 50, 47, 0.25), #113b46);
+ @include diff_background($solarized-dark-old-bg, $solarized-dark-old-idiff, $solarized-dark-border);
}
.line_content.match {
@@ -53,12 +138,12 @@
// highlight line via anchor
pre .hll {
- background-color: #174652 !important;
+ background-color: $solarized-dark-hll-bg !important;
}
// Search result highlight
span.highlight_word {
- background-color: #094554 !important;
+ background-color: $solarized-dark-highlight !important;
}
/* Solarized Dark
@@ -79,72 +164,72 @@
green #859900 operators, other keywords
*/
- .c { color: #586e75; } /* Comment */
- .err { color: #93a1a1; } /* Error */
- .g { color: #93a1a1; } /* Generic */
- .k { color: #859900; } /* Keyword */
- .l { color: #93a1a1; } /* Literal */
- .n { color: #93a1a1; } /* Name */
- .o { color: #859900; } /* Operator */
- .x { color: #cb4b16; } /* Other */
- .p { color: #93a1a1; } /* Punctuation */
- .cm { color: #586e75; } /* Comment.Multiline */
- .cp { color: #859900; } /* Comment.Preproc */
- .c1 { color: #586e75; } /* Comment.Single */
- .cs { color: #859900; } /* Comment.Special */
- .gd { color: #2aa198; } /* Generic.Deleted */
- .ge { color: #93a1a1; font-style: italic; } /* Generic.Emph */
- .gr { color: #dc322f; } /* Generic.Error */
- .gh { color: #cb4b16; } /* Generic.Heading */
- .gi { color: #859900; } /* Generic.Inserted */
- .go { color: #93a1a1; } /* Generic.Output */
- .gp { color: #93a1a1; } /* Generic.Prompt */
- .gs { color: #93a1a1; font-weight: bold; } /* Generic.Strong */
- .gu { color: #cb4b16; } /* Generic.Subheading */
- .gt { color: #93a1a1; } /* Generic.Traceback */
- .kc { color: #cb4b16; } /* Keyword.Constant */
- .kd { color: #268bd2; } /* Keyword.Declaration */
- .kn { color: #859900; } /* Keyword.Namespace */
- .kp { color: #859900; } /* Keyword.Pseudo */
- .kr { color: #268bd2; } /* Keyword.Reserved */
- .kt { color: #dc322f; } /* Keyword.Type */
- .ld { color: #93a1a1; } /* Literal.Date */
- .m { color: #2aa198; } /* Literal.Number */
- .s { color: #2aa198; } /* Literal.String */
- .na { color: #93a1a1; } /* Name.Attribute */
- .nb { color: #b58900; } /* Name.Builtin */
- .nc { color: #268bd2; } /* Name.Class */
- .no { color: #cb4b16; } /* Name.Constant */
- .nd { color: #268bd2; } /* Name.Decorator */
- .ni { color: #cb4b16; } /* Name.Entity */
- .ne { color: #cb4b16; } /* Name.Exception */
- .nf { color: #268bd2; } /* Name.Function */
- .nl { color: #93a1a1; } /* Name.Label */
- .nn { color: #93a1a1; } /* Name.Namespace */
- .nx { color: #93a1a1; } /* Name.Other */
- .py { color: #93a1a1; } /* Name.Property */
- .nt { color: #268bd2; } /* Name.Tag */
- .nv { color: #268bd2; } /* Name.Variable */
- .ow { color: #859900; } /* Operator.Word */
- .w { color: #93a1a1; } /* Text.Whitespace */
- .mf { color: #2aa198; } /* Literal.Number.Float */
- .mh { color: #2aa198; } /* Literal.Number.Hex */
- .mi { color: #2aa198; } /* Literal.Number.Integer */
- .mo { color: #2aa198; } /* Literal.Number.Oct */
- .sb { color: #586e75; } /* Literal.String.Backtick */
- .sc { color: #2aa198; } /* Literal.String.Char */
- .sd { color: #93a1a1; } /* Literal.String.Doc */
- .s2 { color: #2aa198; } /* Literal.String.Double */
- .se { color: #cb4b16; } /* Literal.String.Escape */
- .sh { color: #93a1a1; } /* Literal.String.Heredoc */
- .si { color: #2aa198; } /* Literal.String.Interpol */
- .sx { color: #2aa198; } /* Literal.String.Other */
- .sr { color: #dc322f; } /* Literal.String.Regex */
- .s1 { color: #2aa198; } /* Literal.String.Single */
- .ss { color: #2aa198; } /* Literal.String.Symbol */
- .bp { color: #268bd2; } /* Name.Builtin.Pseudo */
- .vc { color: #268bd2; } /* Name.Variable.Class */
- .vg { color: #268bd2; } /* Name.Variable.Global */
- .vi { color: #268bd2; } /* Name.Variable.Instance */
- .il { color: #2aa198; } /* Literal.Number.Integer.Long */
+ .c { color: $solarized-dark-c; } /* Comment */
+ .err { color: $solarized-dark-err; } /* Error */
+ .g { color: $solarized-dark-g; } /* Generic */
+ .k { color: $solarized-dark-k; } /* Keyword */
+ .l { color: $solarized-dark-l; } /* Literal */
+ .n { color: $solarized-dark-n; } /* Name */
+ .o { color: $solarized-dark-o; } /* Operator */
+ .x { color: $solarized-dark-x; } /* Other */
+ .p { color: $solarized-dark-p; } /* Punctuation */
+ .cm { color: $solarized-dark-cm; } /* Comment.Multiline */
+ .cp { color: $solarized-dark-cp; } /* Comment.Preproc */
+ .c1 { color: $solarized-dark-c1; } /* Comment.Single */
+ .cs { color: $solarized-dark-cs; } /* Comment.Special */
+ .gd { color: $solarized-dark-gd; } /* Generic.Deleted */
+ .ge { color: $solarized-dark-ge; font-style: italic; } /* Generic.Emph */
+ .gr { color: $solarized-dark-gr; } /* Generic.Error */
+ .gh { color: $solarized-dark-gh; } /* Generic.Heading */
+ .gi { color: $solarized-dark-gi; } /* Generic.Inserted */
+ .go { color: $solarized-dark-go; } /* Generic.Output */
+ .gp { color: $solarized-dark-gp; } /* Generic.Prompt */
+ .gs { color: $solarized-dark-gs; font-weight: bold; } /* Generic.Strong */
+ .gu { color: $solarized-dark-gu; } /* Generic.Subheading */
+ .gt { color: $solarized-dark-gt; } /* Generic.Traceback */
+ .kc { color: $solarized-dark-kc; } /* Keyword.Constant */
+ .kd { color: $solarized-dark-kd; } /* Keyword.Declaration */
+ .kn { color: $solarized-dark-kn; } /* Keyword.Namespace */
+ .kp { color: $solarized-dark-kp; } /* Keyword.Pseudo */
+ .kr { color: $solarized-dark-kr; } /* Keyword.Reserved */
+ .kt { color: $solarized-dark-kt; } /* Keyword.Type */
+ .ld { color: $solarized-dark-ld; } /* Literal.Date */
+ .m { color: $solarized-dark-m; } /* Literal.Number */
+ .s { color: $solarized-dark-s; } /* Literal.String */
+ .na { color: $solarized-dark-na; } /* Name.Attribute */
+ .nb { color: $solarized-dark-nb; } /* Name.Builtin */
+ .nc { color: $solarized-dark-nc; } /* Name.Class */
+ .no { color: $solarized-dark-no; } /* Name.Constant */
+ .nd { color: $solarized-dark-nd; } /* Name.Decorator */
+ .ni { color: $solarized-dark-ni; } /* Name.Entity */
+ .ne { color: $solarized-dark-ne; } /* Name.Exception */
+ .nf { color: $solarized-dark-nf; } /* Name.Function */
+ .nl { color: $solarized-dark-nl; } /* Name.Label */
+ .nn { color: $solarized-dark-nn; } /* Name.Namespace */
+ .nx { color: $solarized-dark-nx; } /* Name.Other */
+ .py { color: $solarized-dark-py; } /* Name.Property */
+ .nt { color: $solarized-dark-nt; } /* Name.Tag */
+ .nv { color: $solarized-dark-nv; } /* Name.Variable */
+ .ow { color: $solarized-dark-ow; } /* Operator.Word */
+ .w { color: $solarized-dark-w; } /* Text.Whitespace */
+ .mf { color: $solarized-dark-mf; } /* Literal.Number.Float */
+ .mh { color: $solarized-dark-mh; } /* Literal.Number.Hex */
+ .mi { color: $solarized-dark-mi; } /* Literal.Number.Integer */
+ .mo { color: $solarized-dark-mo; } /* Literal.Number.Oct */
+ .sb { color: $solarized-dark-sb; } /* Literal.String.Backtick */
+ .sc { color: $solarized-dark-sc; } /* Literal.String.Char */
+ .sd { color: $solarized-dark-sd; } /* Literal.String.Doc */
+ .s2 { color: $solarized-dark-s2; } /* Literal.String.Double */
+ .se { color: $solarized-dark-se; } /* Literal.String.Escape */
+ .sh { color: $solarized-dark-sh; } /* Literal.String.Heredoc */
+ .si { color: $solarized-dark-si; } /* Literal.String.Interpol */
+ .sx { color: $solarized-dark-sx; } /* Literal.String.Other */
+ .sr { color: $solarized-dark-sr; } /* Literal.String.Regex */
+ .s1 { color: $solarized-dark-s1; } /* Literal.String.Single */
+ .ss { color: $solarized-dark-ss; } /* Literal.String.Symbol */
+ .bp { color: $solarized-dark-bp; } /* Name.Builtin.Pseudo */
+ .vc { color: $solarized-dark-vc; } /* Name.Variable.Class */
+ .vg { color: $solarized-dark-vg; } /* Name.Variable.Global */
+ .vi { color: $solarized-dark-vi; } /* Name.Variable.Instance */
+ .il { color: $solarized-dark-il; } /* Literal.Number.Integer.Long */
}
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index faff353ded7..499a1c108b8 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -1,15 +1,99 @@
/* https://gist.github.com/qguv/7936275 */
+/*
+* Solarized light syntax colors
+*/
+$solarized-light-matchline-bg: rgba(255, 255, 255, 0.4);
+$solarized-light-new-bg: rgba(133, 153, 0, 0.2);
+$solarized-light-new-idiff: rgba(133, 153, 0, 0.25);
+$solarized-light-old-bg: rgba(220, 50, 47, 0.2);
+$solarized-light-old-idiff: rgba(220, 50, 47, 0.25);
+$solarized-light-border: #c5d0d4;
+$solarized-light-pre-bg: #002b36;
+$solarized-light-pre-bg: #fdf6e3;
+$solarized-light-pre-color: #586e75;
+$solarized-light-line-bg: #fdf6e3;
+$solarized-light-highlight: #eee8d5;
+$solarized-light-hll-bg: #ddd8c5;
+$solarized-light-c: #93a1a1;
+$solarized-light-err: #586e75;
+$solarized-light-g: #586e75;
+$solarized-light-k: #859900;
+$solarized-light-l: #586e75;
+$solarized-light-n: #586e75;
+$solarized-light-o: #859900;
+$solarized-light-x: #cb4b16;
+$solarized-light-p: #586e75;
+$solarized-light-cm: #93a1a1;
+$solarized-light-cp: #859900;
+$solarized-light-c1: #93a1a1;
+$solarized-light-cs: #859900;
+$solarized-light-gd: #2aa198;
+$solarized-light-ge: #586e75;
+$solarized-light-gr: #dc322f;
+$solarized-light-gh: #cb4b16;
+$solarized-light-gi: #859900;
+$solarized-light-go: #586e75;
+$solarized-light-gp: #586e75;
+$solarized-light-gs: #586e75;
+$solarized-light-gu: #cb4b16;
+$solarized-light-gt: #586e75;
+$solarized-light-kc: #cb4b16;
+$solarized-light-kd: #268bd2;
+$solarized-light-kn: #859900;
+$solarized-light-kp: #859900;
+$solarized-light-kr: #268bd2;
+$solarized-light-kt: #dc322f;
+$solarized-light-ld: #586e75;
+$solarized-light-m: #2aa198;
+$solarized-light-s: #2aa198;
+$solarized-light-na: #586e75;
+$solarized-light-nb: #b58900;
+$solarized-light-nc: #268bd2;
+$solarized-light-no: #cb4b16;
+$solarized-light-nd: #268bd2;
+$solarized-light-ni: #cb4b16;
+$solarized-light-ne: #cb4b16;
+$solarized-light-nf: #268bd2;
+$solarized-light-nl: #586e75;
+$solarized-light-nn: #586e75;
+$solarized-light-nx: #586e75;
+$solarized-light-py: #586e75;
+$solarized-light-nt: #268bd2;
+$solarized-light-nv: #268bd2;
+$solarized-light-ow: #859900;
+$solarized-light-w: #586e75;
+$solarized-light-mf: #2aa198;
+$solarized-light-mh: #2aa198;
+$solarized-light-mi: #2aa198;
+$solarized-light-mo: #2aa198;
+$solarized-light-sb: #93a1a1;
+$solarized-light-sc: #2aa198;
+$solarized-light-sd: #586e75;
+$solarized-light-s2: #2aa198;
+$solarized-light-se: #cb4b16;
+$solarized-light-sh: #586e75;
+$solarized-light-si: #2aa198;
+$solarized-light-sx: #2aa198;
+$solarized-light-sr: #dc322f;
+$solarized-light-s1: #2aa198;
+$solarized-light-ss: #2aa198;
+$solarized-light-bp: #268bd2;
+$solarized-light-vc: #268bd2;
+$solarized-light-vg: #268bd2;
+$solarized-light-vi: #268bd2;
+$solarized-light-il: #2aa198;
+
@mixin matchLine {
color: $black-transparent;
- background: rgba(255, 255, 255, 0.4);
+ background: $solarized-light-matchline-bg;
}
.code.solarized-light {
// Line numbers
.line-numbers,
.diff-line-num {
- background-color: #fdf6e3;
+ background-color: $solarized-light-line-bg;
}
.diff-line-num,
@@ -20,14 +104,14 @@
// Code itself
pre.code,
.diff-line-num {
- border-color: #c5d0d4;
+ border-color: $solarized-light-border;
}
&,
pre.code,
.line_holder .line_content {
- background-color: #fdf6e3;
- color: #586e75;
+ background-color: $solarized-light-pre-bg;
+ color: $solarized-light-pre-color;
}
// Diff line
@@ -38,18 +122,19 @@
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
- background-color: #ddd8c5;
- border-color: darken(#ddd8c5, 15%);
+ background-color: $solarized-light-hll-bg;
+ border-color: darken($solarized-light-hll-bg, 15%);
}
.diff-line-num.new,
.line_content.new {
- @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.25), #c5d0d4);
+ @include diff_background($solarized-light-new-bg,
+ $solarized-light-new-idiff, $solarized-light-border);
}
.diff-line-num.old,
.line_content.old {
- @include diff_background(rgba(220, 50, 47, 0.2), rgba(220, 50, 47, 0.25), #c5d0d4);
+ @include diff_background($solarized-light-old-bg, $solarized-light-old-idiff, $solarized-light-border);
}
.line_content.match {
@@ -59,12 +144,12 @@
// highlight line via anchor
pre .hll {
- background-color: #ddd8c5 !important;
+ background-color: $solarized-light-hll-bg !important;
}
// Search result highlight
span.highlight_word {
- background-color: #eee8d5 !important;
+ background-color: $solarized-light-highlight !important;
}
/* Solarized Light
@@ -85,72 +170,72 @@
green #859900 operators, other keywords
*/
- .c { color: #93a1a1; } /* Comment */
- .err { color: #586e75; } /* Error */
- .g { color: #586e75; } /* Generic */
- .k { color: #859900; } /* Keyword */
- .l { color: #586e75; } /* Literal */
- .n { color: #586e75; } /* Name */
- .o { color: #859900; } /* Operator */
- .x { color: #cb4b16; } /* Other */
- .p { color: #586e75; } /* Punctuation */
- .cm { color: #93a1a1; } /* Comment.Multiline */
- .cp { color: #859900; } /* Comment.Preproc */
- .c1 { color: #93a1a1; } /* Comment.Single */
- .cs { color: #859900; } /* Comment.Special */
- .gd { color: #2aa198; } /* Generic.Deleted */
- .ge { color: #586e75; font-style: italic; } /* Generic.Emph */
- .gr { color: #dc322f; } /* Generic.Error */
- .gh { color: #cb4b16; } /* Generic.Heading */
- .gi { color: #859900; } /* Generic.Inserted */
- .go { color: #586e75; } /* Generic.Output */
- .gp { color: #586e75; } /* Generic.Prompt */
- .gs { color: #586e75; font-weight: bold; } /* Generic.Strong */
- .gu { color: #cb4b16; } /* Generic.Subheading */
- .gt { color: #586e75; } /* Generic.Traceback */
- .kc { color: #cb4b16; } /* Keyword.Constant */
- .kd { color: #268bd2; } /* Keyword.Declaration */
- .kn { color: #859900; } /* Keyword.Namespace */
- .kp { color: #859900; } /* Keyword.Pseudo */
- .kr { color: #268bd2; } /* Keyword.Reserved */
- .kt { color: #dc322f; } /* Keyword.Type */
- .ld { color: #586e75; } /* Literal.Date */
- .m { color: #2aa198; } /* Literal.Number */
- .s { color: #2aa198; } /* Literal.String */
- .na { color: #586e75; } /* Name.Attribute */
- .nb { color: #b58900; } /* Name.Builtin */
- .nc { color: #268bd2; } /* Name.Class */
- .no { color: #cb4b16; } /* Name.Constant */
- .nd { color: #268bd2; } /* Name.Decorator */
- .ni { color: #cb4b16; } /* Name.Entity */
- .ne { color: #cb4b16; } /* Name.Exception */
- .nf { color: #268bd2; } /* Name.Function */
- .nl { color: #586e75; } /* Name.Label */
- .nn { color: #586e75; } /* Name.Namespace */
- .nx { color: #586e75; } /* Name.Other */
- .py { color: #586e75; } /* Name.Property */
- .nt { color: #268bd2; } /* Name.Tag */
- .nv { color: #268bd2; } /* Name.Variable */
- .ow { color: #859900; } /* Operator.Word */
- .w { color: #586e75; } /* Text.Whitespace */
- .mf { color: #2aa198; } /* Literal.Number.Float */
- .mh { color: #2aa198; } /* Literal.Number.Hex */
- .mi { color: #2aa198; } /* Literal.Number.Integer */
- .mo { color: #2aa198; } /* Literal.Number.Oct */
- .sb { color: #93a1a1; } /* Literal.String.Backtick */
- .sc { color: #2aa198; } /* Literal.String.Char */
- .sd { color: #586e75; } /* Literal.String.Doc */
- .s2 { color: #2aa198; } /* Literal.String.Double */
- .se { color: #cb4b16; } /* Literal.String.Escape */
- .sh { color: #586e75; } /* Literal.String.Heredoc */
- .si { color: #2aa198; } /* Literal.String.Interpol */
- .sx { color: #2aa198; } /* Literal.String.Other */
- .sr { color: #dc322f; } /* Literal.String.Regex */
- .s1 { color: #2aa198; } /* Literal.String.Single */
- .ss { color: #2aa198; } /* Literal.String.Symbol */
- .bp { color: #268bd2; } /* Name.Builtin.Pseudo */
- .vc { color: #268bd2; } /* Name.Variable.Class */
- .vg { color: #268bd2; } /* Name.Variable.Global */
- .vi { color: #268bd2; } /* Name.Variable.Instance */
- .il { color: #2aa198; } /* Literal.Number.Integer.Long */
+ .c { color: $solarized-light-c; } /* Comment */
+ .err { color: $solarized-light-err; } /* Error */
+ .g { color: $solarized-light-g; } /* Generic */
+ .k { color: $solarized-light-k; } /* Keyword */
+ .l { color: $solarized-light-l; } /* Literal */
+ .n { color: $solarized-light-n; } /* Name */
+ .o { color: $solarized-light-o; } /* Operator */
+ .x { color: $solarized-light-x; } /* Other */
+ .p { color: $solarized-light-p; } /* Punctuation */
+ .cm { color: $solarized-light-cm; } /* Comment.Multiline */
+ .cp { color: $solarized-light-cp; } /* Comment.Preproc */
+ .c1 { color: $solarized-light-c1; } /* Comment.Single */
+ .cs { color: $solarized-light-cs; } /* Comment.Special */
+ .gd { color: $solarized-light-gd; } /* Generic.Deleted */
+ .ge { color: $solarized-light-ge; font-style: italic; } /* Generic.Emph */
+ .gr { color: $solarized-light-gr; } /* Generic.Error */
+ .gh { color: $solarized-light-gh; } /* Generic.Heading */
+ .gi { color: $solarized-light-gi; } /* Generic.Inserted */
+ .go { color: $solarized-light-go; } /* Generic.Output */
+ .gp { color: $solarized-light-gp; } /* Generic.Prompt */
+ .gs { color: $solarized-light-gs; font-weight: bold; } /* Generic.Strong */
+ .gu { color: $solarized-light-gu; } /* Generic.Subheading */
+ .gt { color: $solarized-light-gt; } /* Generic.Traceback */
+ .kc { color: $solarized-light-kc; } /* Keyword.Constant */
+ .kd { color: $solarized-light-kd; } /* Keyword.Declaration */
+ .kn { color: $solarized-light-kn; } /* Keyword.Namespace */
+ .kp { color: $solarized-light-kp; } /* Keyword.Pseudo */
+ .kr { color: $solarized-light-kr; } /* Keyword.Reserved */
+ .kt { color: $solarized-light-kt; } /* Keyword.Type */
+ .ld { color: $solarized-light-ld; } /* Literal.Date */
+ .m { color: $solarized-light-m; } /* Literal.Number */
+ .s { color: $solarized-light-s; } /* Literal.String */
+ .na { color: $solarized-light-na; } /* Name.Attribute */
+ .nb { color: $solarized-light-nb; } /* Name.Builtin */
+ .nc { color: $solarized-light-nc; } /* Name.Class */
+ .no { color: $solarized-light-no; } /* Name.Constant */
+ .nd { color: $solarized-light-nd; } /* Name.Decorator */
+ .ni { color: $solarized-light-ni; } /* Name.Entity */
+ .ne { color: $solarized-light-ne; } /* Name.Exception */
+ .nf { color: $solarized-light-nf; } /* Name.Function */
+ .nl { color: $solarized-light-nl; } /* Name.Label */
+ .nn { color: $solarized-light-nn; } /* Name.Namespace */
+ .nx { color: $solarized-light-nx; } /* Name.Other */
+ .py { color: $solarized-light-py; } /* Name.Property */
+ .nt { color: $solarized-light-nt; } /* Name.Tag */
+ .nv { color: $solarized-light-nv; } /* Name.Variable */
+ .ow { color: $solarized-light-ow; } /* Operator.Word */
+ .w { color: $solarized-light-w; } /* Text.Whitespace */
+ .mf { color: $solarized-light-mf; } /* Literal.Number.Float */
+ .mh { color: $solarized-light-mh; } /* Literal.Number.Hex */
+ .mi { color: $solarized-light-mi; } /* Literal.Number.Integer */
+ .mo { color: $solarized-light-mo; } /* Literal.Number.Oct */
+ .sb { color: $solarized-light-sb; } /* Literal.String.Backtick */
+ .sc { color: $solarized-light-sc; } /* Literal.String.Char */
+ .sd { color: $solarized-light-sd; } /* Literal.String.Doc */
+ .s2 { color: $solarized-light-s2; } /* Literal.String.Double */
+ .se { color: $solarized-light-se; } /* Literal.String.Escape */
+ .sh { color: $solarized-light-sh; } /* Literal.String.Heredoc */
+ .si { color: $solarized-light-si; } /* Literal.String.Interpol */
+ .sx { color: $solarized-light-sx; } /* Literal.String.Other */
+ .sr { color: $solarized-light-sr; } /* Literal.String.Regex */
+ .s1 { color: $solarized-light-s1; } /* Literal.String.Single */
+ .ss { color: $solarized-light-ss; } /* Literal.String.Symbol */
+ .bp { color: $solarized-light-bp; } /* Name.Builtin.Pseudo */
+ .vc { color: $solarized-light-vc; } /* Name.Variable.Class */
+ .vg { color: $solarized-light-vg; } /* Name.Variable.Global */
+ .vi { color: $solarized-light-vi; } /* Name.Variable.Instance */
+ .il { color: $solarized-light-il; } /* Literal.Number.Integer.Long */
}
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index d5367d5f3f0..54a5664a874 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -1,15 +1,82 @@
/* https://github.com/aahan/pygments-github-style */
+/*
+* White Syntax Colors
+*/
+$white-code-color: #333;
+$white-highlight: #fafe3d;
+$white-pre-hll-bg: #f8eec7;
+$white-hll-bg: #f8f8f8;
+$white-c: #998;
+$white-err: #a61717;
+$white-err-bg: #e3d2d2;
+$white-cm: #998;
+$white-cp: #999;
+$white-c1: #998;
+$white-cs: #999;
+$white-gd: $black;
+$white-gd-bg: #fdd;
+$white-gd-x: $black;
+$white-gd-x-bg: #faa;
+$white-gr: #a00;
+$white-gh: #999;
+$white-gi: $black;
+$white-gi-bg: #dfd;
+$white-gi-x: $black;
+$white-gi-x-bg: #afa;
+$white-go: #888;
+$white-gp: #555;
+$white-gu: #800080;
+$white-gt: #a00;
+$white-kt: #458;
+$white-m: #099;
+$white-s: #d14;
+$white-n: #333;
+$white-na: teal;
+$white-nb: #0086b3;
+$white-nc: #458;
+$white-no: teal;
+$white-ni: purple;
+$white-ne: #900;
+$white-nf: #900;
+$white-nn: #555;
+$white-nt: navy;
+$white-nv: teal;
+$white-w: #bbb;
+$white-mf: #099;
+$white-mh: #099;
+$white-mi: #099;
+$white-mo: #099;
+$white-sb: #d14;
+$white-sc: #d14;
+$white-sd: #d14;
+$white-s2: #d14;
+$white-se: #d14;
+$white-sh: #d14;
+$white-si: #d14;
+$white-sx: #d14;
+$white-sr: #009926;
+$white-s1: #d14;
+$white-ss: #990073;
+$white-bp: #999;
+$white-vc: teal;
+$white-vg: teal;
+$white-vi: teal;
+$white-il: #099;
+$white-gc-color: #999;
+$white-gc-bg: #eaf2f5;
+
+
@mixin matchLine {
color: $black-transparent;
- background-color: $match-line;
+ background-color: $gray-light;
}
.code.white {
// Line numbers
.line-numbers,
.diff-line-num {
- background-color: $background-color;
+ background-color: $gray-light;
}
.diff-line-num,
@@ -20,14 +87,14 @@
// Code itself
pre.code,
.diff-line-num {
- border-color: $table-border-gray;
+ border-color: $white-normal;
}
&,
pre.code,
.line_holder .line_content {
- background-color: #fff;
- color: #333;
+ background-color: $white-light;
+ color: $white-code-color;
}
// Diff line
@@ -83,75 +150,75 @@
// highlight line via anchor
pre .hll {
- background-color: #f8eec7 !important;
+ background-color: $white-pre-hll-bg !important;
}
// Search result highlight
span.highlight_word {
- background-color: #fafe3d !important;
+ background-color: $white-highlight !important;
}
- .hll { background-color: #f8f8f8; }
- .c { color: #998; font-style: italic; }
- .err { color: #a61717; background-color: #e3d2d2; }
+ .hll { background-color: $white-hll-bg; }
+ .c { color: $white-c; font-style: italic; }
+ .err { color: $white-err; background-color: $white-err-bg; }
.k { font-weight: bold; }
.o { font-weight: bold; }
- .cm { color: #998; font-style: italic; }
- .cp { color: #999; font-weight: bold; }
- .c1 { color: #998; font-style: italic; }
- .cs { color: #999; font-weight: bold; font-style: italic; }
- .gd { color: #000; background-color: #fdd; }
- .gd .x { color: #000; background-color: #faa; }
+ .cm { color: $white-cm; font-style: italic; }
+ .cp { color: $white-cp; font-weight: bold; }
+ .c1 { color: $white-c1; font-style: italic; }
+ .cs { color: $white-cs; font-weight: bold; font-style: italic; }
+ .gd { color: $white-gd; background-color: $white-gd-bg; }
+ .gd .x { color: $white-gd-x; background-color: $white-gd-x-bg; }
.ge { font-style: italic; }
- .gr { color: #a00; }
- .gh { color: #999; }
- .gi { color: #000; background-color: #dfd; }
- .gi .x { color: #000; background-color: #afa; }
- .go { color: #888; }
- .gp { color: #555; }
+ .gr { color: $white-gr; }
+ .gh { color: $white-gh; }
+ .gi { color: $white-gi; background-color: $white-gi-bg; }
+ .gi .x { color: $white-gi-x; background-color: $white-gi-x-bg; }
+ .go { color: $white-go; }
+ .gp { color: $white-gp; }
.gs { font-weight: bold; }
- .gu { color: #800080; font-weight: bold; }
- .gt { color: #a00; }
+ .gu { color: $white-gu; font-weight: bold; }
+ .gt { color: $white-gt; }
.kc { font-weight: bold; }
.kd { font-weight: bold; }
.kn { font-weight: bold; }
.kp { font-weight: bold; }
.kr { font-weight: bold; }
- .kt { color: #458; font-weight: bold; }
- .m { color: #099; }
- .s { color: #d14; }
- .n { color: #333; }
- .na { color: teal; }
- .nb { color: #0086b3; }
- .nc { color: #458; font-weight: bold; }
- .no { color: teal; }
- .ni { color: purple; }
- .ne { color: #900; font-weight: bold; }
- .nf { color: #900; font-weight: bold; }
- .nn { color: #555; }
- .nt { color: navy; }
- .nv { color: teal; }
+ .kt { color: $white-kt; font-weight: bold; }
+ .m { color: $white-m; }
+ .s { color: $white-s; }
+ .n { color: $white-n; }
+ .na { color: $white-na; }
+ .nb { color: $white-nb; }
+ .nc { color: $white-nc; font-weight: bold; }
+ .no { color: $white-no; }
+ .ni { color: $white-ni; }
+ .ne { color: $white-ne; font-weight: bold; }
+ .nf { color: $white-nf; font-weight: bold; }
+ .nn { color: $white-nn; }
+ .nt { color: $white-nt; }
+ .nv { color: $white-nv; }
.ow { font-weight: bold; }
- .w { color: #bbb; }
- .mf { color: #099; }
- .mh { color: #099; }
- .mi { color: #099; }
- .mo { color: #099; }
- .sb { color: #d14; }
- .sc { color: #d14; }
- .sd { color: #d14; }
- .s2 { color: #d14; }
- .se { color: #d14; }
- .sh { color: #d14; }
- .si { color: #d14; }
- .sx { color: #d14; }
- .sr { color: #009926; }
- .s1 { color: #d14; }
- .ss { color: #990073; }
- .bp { color: #999; }
- .vc { color: teal; }
- .vg { color: teal; }
- .vi { color: teal; }
- .il { color: #099; }
- .gc { color: #999; background-color: #eaf2f5; }
+ .w { color: $white-w; }
+ .mf { color: $white-mf; }
+ .mh { color: $white-mh; }
+ .mi { color: $white-mi; }
+ .mo { color: $white-mo; }
+ .sb { color: $white-sb; }
+ .sc { color: $white-sc; }
+ .sd { color: $white-sd; }
+ .s2 { color: $white-s2; }
+ .se { color: $white-se; }
+ .sh { color: $white-sh; }
+ .si { color: $white-si; }
+ .sx { color: $white-sx; }
+ .sr { color: $white-sr; }
+ .s1 { color: $white-s1; }
+ .ss { color: $white-ss; }
+ .bp { color: $white-bp; }
+ .vc { color: $white-vc; }
+ .vg { color: $white-vg; }
+ .vi { color: $white-vi; }
+ .il { color: $white-il; }
+ .gc { color: $white-gc-color; background-color: $white-gc-bg; }
}
diff --git a/app/assets/stylesheets/mailers/devise.scss b/app/assets/stylesheets/mailers/devise.scss
index b2bce482fde..9f613710cf4 100644
--- a/app/assets/stylesheets/mailers/devise.scss
+++ b/app/assets/stylesheets/mailers/devise.scss
@@ -1,3 +1,5 @@
+@import "framework/variables";
+
// NOTE: This stylesheet is for the exclusive use of the `devise_mailer` layout
// used for Devise email templates, and _should not_ be included in any
// application stylesheets.
@@ -46,7 +48,7 @@ table {
&#body {
background-color: $message-background-color;
- border: 1px solid #000;
+ border: 1px solid $black;
border-radius: 4px;
margin: 0 auto;
width: 600px;
diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
new file mode 100644
index 00000000000..60ff72c703e
--- /dev/null
+++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
@@ -0,0 +1,207 @@
+@import "framework/variables";
+
+// This file is largely copied from `highlight/white.scss`, but modified to
+// avoid all descendant selectors (`table td`). This is because the CSS inlining
+// we use performs dramatically worse on descendant selectors than the
+// alternatives.
+// <https://gitlab.com/gitlab-org/gitlab-ee/issues/490#note_12283632>
+//
+// DO NOT ADD ANY DESCENDANT SELECTORS TO THIS FILE. Instead, use (in order of
+// preference): plain class selectors, type (element name) selectors, or
+// explicit child selectors.
+
+/*
+* Highlighted Diff Email Syntax Colors
+*/
+$highlighted-highlight-word: #fafe3d;
+$highlighted-hll-bg: #f8f8f8;
+$highlighted-c: #998;
+$highlighted-err: #a61717;
+$highlighted-err-bg: #e3d2d2;
+$highlighted-cm: #998;
+$highlighted-cp: #999;
+$highlighted-c1: #998;
+$highlighted-cs: #999;
+$highlighted-gd: #000;
+$highlighted-gd-bg: #fdd;
+$highlighted-gd-x: #000;
+$highlighted-gd-x-bg: #faa;
+$highlighted-gr: #a00;
+$highlighted-gh: #999;
+$highlighted-gi: #000;
+$highlighted-gi-bg: #dfd;
+$highlighted-gi-x: #000;
+$highlighted-gi-x-bg: #afa;
+$highlighted-go: #888;
+$highlighted-gp: #555;
+$highlighted-gu: #800080;
+$highlighted-gt: #a00;
+$highlighted-kt: #458;
+$highlighted-m: #099;
+$highlighted-s: #d14;
+$highlighted-n: #333;
+$highlighted-na: teal;
+$highlighted-nb: #0086b3;
+$highlighted-nc: #458;
+$highlighted-no: teal;
+$highlighted-ni: purple;
+$highlighted-ne: #900;
+$highlighted-nf: #900;
+$highlighted-nn: #555;
+$highlighted-nt: navy;
+$highlighted-nv: teal;
+$highlighted-w: #bbb;
+$highlighted-mf: #099;
+$highlighted-mh: #099;
+$highlighted-mi: #099;
+$highlighted-mo: #099;
+$highlighted-sb: #d14;
+$highlighted-sc: #d14;
+$highlighted-sd: #d14;
+$highlighted-s2: #d14;
+$highlighted-se: #d14;
+$highlighted-sh: #d14;
+$highlighted-si: #d14;
+$highlighted-sx: #d14;
+$highlighted-sr: #009926;
+$highlighted-s1: #d14;
+$highlighted-ss: #990073;
+$highlighted-bp: #999;
+$highlighted-vc: teal;
+$highlighted-vg: teal;
+$highlighted-vi: teal;
+$highlighted-il: #099;
+$highlighted-gc: #999;
+$highlighted-gc-bg: #eaf2f5;
+
+.code {
+ background-color: $white-light;
+ font-family: monospace;
+ font-size: $code_font_size;
+ -premailer-cellpadding: 0;
+ -premailer-cellspacing: 0;
+ -premailer-width: 100%;
+
+ > tr {
+ line-height: $code_line_height;
+ }
+}
+
+.diff-line-num {
+ padding: 0 5px;
+ text-align: right;
+ width: 35px;
+ background-color: $gray-light;
+ color: $black-transparent;
+ border-right: 1px solid $white-normal;
+
+ &.old {
+ background-color: $line-number-old;
+ border-right-color: $line-removed-dark;
+ }
+
+ &.new {
+ background-color: $line-number-new;
+ border-right-color: $line-added-dark;
+ }
+}
+
+.line_content {
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+
+ &.old {
+ background-color: $line-removed;
+
+ > .line > span.idiff,
+ > .line > span > span.idiff {
+ background-color: $line-removed-dark;
+ }
+ }
+
+ &.new {
+ background-color: $line-added;
+
+ > .line > span.idiff,
+ > .line > span > span.idiff {
+ background-color: $line-added-dark;
+ }
+ }
+
+ &.match {
+ color: $black-transparent;
+ background-color: $gray-light;
+ }
+}
+
+pre {
+ margin: 0;
+}
+
+span.highlight_word {
+ background-color: $highlighted-highlight-word !important;
+}
+
+.hll { background-color: $highlighted-hll-bg; }
+.c { color: $highlighted-c; font-style: italic; }
+.err { color: $highlighted-err; background-color: $highlighted-err-bg; }
+.k { font-weight: bold; }
+.o { font-weight: bold; }
+.cm { color: $highlighted-cm; font-style: italic; }
+.cp { color: $highlighted-cp; font-weight: bold; }
+.c1 { color: $highlighted-c1; font-style: italic; }
+.cs { color: $highlighted-cs; font-weight: bold; font-style: italic; }
+.gd { color: $highlighted-gd; background-color: $highlighted-gd-bg; }
+.gd .x { color: $highlighted-gd; background-color: $highlighted-gd-x-bg; }
+.ge { font-style: italic; }
+.gr { color: $highlighted-gr; }
+.gh { color: $highlighted-gh; }
+.gi { color: $highlighted-gi; background-color: $highlighted-gi-bg; }
+.gi .x { color: $highlighted-gi; background-color: $highlighted-gi-x-bg; }
+.go { color: $highlighted-go; }
+.gp { color: $highlighted-gp; }
+.gs { font-weight: bold; }
+.gu { color: $highlighted-gu; font-weight: bold; }
+.gt { color: $highlighted-gt; }
+.kc { font-weight: bold; }
+.kd { font-weight: bold; }
+.kn { font-weight: bold; }
+.kp { font-weight: bold; }
+.kr { font-weight: bold; }
+.kt { color: $highlighted-kt; font-weight: bold; }
+.m { color: $highlighted-m; }
+.s { color: $highlighted-s; }
+.n { color: $highlighted-n; }
+.na { color: $highlighted-na; }
+.nb { color: $highlighted-nb; }
+.nc { color: $highlighted-nc; font-weight: bold; }
+.no { color: $highlighted-no; }
+.ni { color: $highlighted-ni; }
+.ne { color: $highlighted-ne; font-weight: bold; }
+.nf { color: $highlighted-nf; font-weight: bold; }
+.nn { color: $highlighted-nn; }
+.nt { color: $highlighted-nt; }
+.nv { color: $highlighted-nv; }
+.ow { font-weight: bold; }
+.w { color: $highlighted-w; }
+.mf { color: $highlighted-mf; }
+.mh { color: $highlighted-mh; }
+.mi { color: $highlighted-mi; }
+.mo { color: $highlighted-mo; }
+.sb { color: $highlighted-sb; }
+.sc { color: $highlighted-sc; }
+.sd { color: $highlighted-sd; }
+.s2 { color: $highlighted-s2; }
+.se { color: $highlighted-se; }
+.sh { color: $highlighted-sh; }
+.si { color: $highlighted-si; }
+.sx { color: $highlighted-sx; }
+.sr { color: $highlighted-sr; }
+.s1 { color: $highlighted-s1; }
+.ss { color: $highlighted-ss; }
+.bp { color: $highlighted-bp; }
+.vc { color: $highlighted-vc; }
+.vg { color: $highlighted-vg; }
+.vi { color: $highlighted-vi; }
+.il { color: $highlighted-il; }
+.gc { color: $highlighted-gc; background-color: $highlighted-gc-bg; }
diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss
deleted file mode 100644
index 8d1a6020ca4..00000000000
--- a/app/assets/stylesheets/mailers/repository_push_email.scss
+++ /dev/null
@@ -1,143 +0,0 @@
-@import "framework/variables";
-
-// This file is largely copied from `highlight/white.scss`, but modified to
-// avoid all descendant selectors (`table td`). This is because the CSS inlining
-// we use performs dramatically worse on descendant selectors than the
-// alternatives.
-// <https://gitlab.com/gitlab-org/gitlab-ee/issues/490#note_12283632>
-//
-// DO NOT ADD ANY DESCENDANT SELECTORS TO THIS FILE. Instead, use (in order of
-// preference): plain class selectors, type (element name) selectors, or
-// explicit child selectors.
-
-.code {
- background-color: #fff;
- font-family: monospace;
- font-size: $code_font_size;
- -premailer-cellpadding: 0;
- -premailer-cellspacing: 0;
- -premailer-width: 100%;
-
- > tr {
- line-height: $code_line_height;
- }
-}
-
-.diff-line-num {
- padding: 0 5px;
- text-align: right;
- width: 35px;
- background-color: $background-color;
- color: $black-transparent;
- border-right: 1px solid $table-border-gray;
-
- &.old {
- background-color: $line-number-old;
- border-right-color: $line-removed-dark;
- }
-
- &.new {
- background-color: $line-number-new;
- border-right-color: $line-added-dark;
- }
-}
-
-.line_content {
- padding-left: 0.5em;
- padding-right: 0.5em;
-
- &.old {
- background-color: $line-removed;
-
- > .line > span.idiff,
- > .line > span > span.idiff {
- background-color: $line-removed-dark;
- }
- }
-
- &.new {
- background-color: $line-added;
-
- > .line > span.idiff,
- > .line > span > span.idiff {
- background-color: $line-added-dark;
- }
- }
-
- &.match {
- color: $black-transparent;
- background-color: $match-line;
- }
-}
-
-pre {
- margin: 0;
-}
-
-span.highlight_word {
- background-color: #fafe3d !important;
-}
-
-.hll { background-color: #f8f8f8; }
-.c { color: #998; font-style: italic; }
-.err { color: #a61717; background-color: #e3d2d2; }
-.k { font-weight: bold; }
-.o { font-weight: bold; }
-.cm { color: #998; font-style: italic; }
-.cp { color: #999; font-weight: bold; }
-.c1 { color: #998; font-style: italic; }
-.cs { color: #999; font-weight: bold; font-style: italic; }
-.gd { color: #000; background-color: #fdd; }
-.gd .x { color: #000; background-color: #faa; }
-.ge { font-style: italic; }
-.gr { color: #a00; }
-.gh { color: #999; }
-.gi { color: #000; background-color: #dfd; }
-.gi .x { color: #000; background-color: #afa; }
-.go { color: #888; }
-.gp { color: #555; }
-.gs { font-weight: bold; }
-.gu { color: #800080; font-weight: bold; }
-.gt { color: #a00; }
-.kc { font-weight: bold; }
-.kd { font-weight: bold; }
-.kn { font-weight: bold; }
-.kp { font-weight: bold; }
-.kr { font-weight: bold; }
-.kt { color: #458; font-weight: bold; }
-.m { color: #099; }
-.s { color: #d14; }
-.n { color: #333; }
-.na { color: teal; }
-.nb { color: #0086b3; }
-.nc { color: #458; font-weight: bold; }
-.no { color: teal; }
-.ni { color: purple; }
-.ne { color: #900; font-weight: bold; }
-.nf { color: #900; font-weight: bold; }
-.nn { color: #555; }
-.nt { color: navy; }
-.nv { color: teal; }
-.ow { font-weight: bold; }
-.w { color: #bbb; }
-.mf { color: #099; }
-.mh { color: #099; }
-.mi { color: #099; }
-.mo { color: #099; }
-.sb { color: #d14; }
-.sc { color: #d14; }
-.sd { color: #d14; }
-.s2 { color: #d14; }
-.se { color: #d14; }
-.sh { color: #d14; }
-.si { color: #d14; }
-.sx { color: #d14; }
-.sr { color: #009926; }
-.s1 { color: #d14; }
-.ss { color: #990073; }
-.bp { color: #999; }
-.vc { color: teal; }
-.vg { color: teal; }
-.vi { color: teal; }
-.il { color: #099; }
-.gc { color: #999; background-color: #eaf2f5; }
diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss
index ced8c4a9907..ddc382362f7 100644
--- a/app/assets/stylesheets/notify.scss
+++ b/app/assets/stylesheets/notify.scss
@@ -1,3 +1,5 @@
+@import "framework/variables";
+
img {
max-width: 100%;
height: auto;
@@ -5,12 +7,12 @@ img {
p.details {
font-style: italic;
- color: #777;
+ color: $notify-details;
}
.footer > p {
font-size: small;
- color: #777;
+ color: $notify-footer;
}
pre.commit-message {
@@ -21,10 +23,10 @@ pre.commit-message {
text-decoration: none;
> .new-file {
- color: #090;
+ color: $notify-new-file;
}
> .deleted-file {
- color: #b00;
+ color: $notify-deleted-file;
}
}
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
deleted file mode 100644
index 6cefafd8fc7..00000000000
--- a/app/assets/stylesheets/pages/admin.scss
+++ /dev/null
@@ -1,162 +0,0 @@
-/**
- * Admin area
- *
- */
-.admin-dashboard {
- .data {
- a {
- h1 {
- line-height: 48px;
- font-size: 48px;
- padding: 20px;
- text-align: center;
- font-weight: normal;
- }
- }
- }
-
- .str-truncated {
- max-width: 60%;
- }
-}
-
-.admin-filter form {
- .select2-container {
- width: 100%;
- }
-
- .controls {
- margin-left: 130px;
- }
-
- .form-actions {
- padding-left: 130px;
- background: #fff;
- }
-
- .visibility-levels {
- .controls {
- margin-bottom: 9px;
- }
-
- i {
- color: inherit;
- }
- }
-}
-
-.broadcast-messages {
- .message {
- line-height: 2;
- }
-}
-
-.broadcast-message {
- @extend .alert-warning;
- padding: 10px;
- text-align: center;
-
- > div,
- p {
- display: inline;
- margin: 0;
-
- a {
- color: inherit;
- text-decoration: underline;
- }
- }
-}
-
-.broadcast-message-preview {
- @extend .broadcast-message;
- margin-bottom: 20px;
-}
-
-// Users List
-
-.users-list {
- .user-row {
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- white-space: nowrap;
- }
-
- .user-details {
- flex: 1 1 auto;
- overflow: hidden;
- padding-right: 8px;
- }
-
- .user-name {
- display: inline-block;
- font-weight: 600;
- }
-
- .user-name,
- .user-email {
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .dropdown {
- .btn-block {
- margin-bottom: 0;
- line-height: inherit;
- }
- }
-
- .label-default {
- color: $btn-transparent-color;
- }
-}
-
-.abuse-reports {
- .table {
- table-layout: fixed;
- }
-
- .subheading {
- padding-bottom: $gl-padding;
- }
-
- .message {
- word-wrap: break-word;
- }
-
- .btn {
- white-space: normal;
- padding: $gl-btn-padding;
- }
-
- th {
- width: 15%;
-
- &.wide {
- width: 55%;
- }
- }
-
- @media (max-width: $screen-sm-max) {
- th {
- width: 100%;
- }
-
- td {
- width: 100%;
- float: left;
- }
- }
-
- .no-reports {
- .emoji-icon {
- margin-left: $btn-side-margin;
- margin-top: 3px;
- }
-
- span {
- font-size: 19px;
- }
- }
-}
diff --git a/app/assets/stylesheets/pages/appearances.scss b/app/assets/stylesheets/pages/appearances.scss
deleted file mode 100644
index 878f44116ba..00000000000
--- a/app/assets/stylesheets/pages/appearances.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-.appearance-logo-preview {
- max-width: 400px;
- margin-bottom: 20px;
-}
-
-.appearance-light-logo-preview {
- background-color: $background-color;
- max-width: 72px;
- padding: 10px;
- margin-bottom: 10px;
-}
diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss
deleted file mode 100644
index 486ad16ea26..00000000000
--- a/app/assets/stylesheets/pages/awards.scss
+++ /dev/null
@@ -1,140 +0,0 @@
-.awards {
- .emoji-icon {
- width: 19px;
- height: 19px;
- }
-}
-
-.emoji-menu {
- position: absolute;
- margin-top: 3px;
- padding: $gl-padding;
- z-index: 9;
- width: 300px;
- font-size: 14px;
- background-color: $award-emoji-menu-bg;
- border: 1px solid $award-emoji-menu-border;
- border-radius: $border-radius-base;
- box-shadow: 0 6px 12px rgba(0,0,0,.175);
- pointer-events: none;
- opacity: 0;
- transform: scale(.2);
- transform-origin: 0 -45px;
- transition: .3s cubic-bezier(.87,-.41,.19,1.44);
- transition-property: transform, opacity;
-
- &.is-aligned-right {
- transform-origin: 100% -45px;
- }
-
- &.is-visible {
- pointer-events: all;
- opacity: 1;
- transform: scale(1);
- }
-
- .emoji-menu-content {
- height: 300px;
- overflow-y: scroll;
- }
-}
-
-.emoji-search {
- background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC");
- background-repeat: no-repeat;
- background-position: right 5px center;
- background-size: 16px;
-}
-
-.emoji-menu-list {
- list-style: none;
- padding-left: 0;
- margin-bottom: 0;
-}
-
-.emoji-menu-list-item {
- padding: 3px;
- margin-left: 1px;
- margin-right: 1px;
-}
-
-.emoji-menu-btn {
- display: block;
- cursor: pointer;
- width: 30px;
- height: 30px;
- padding: 0;
- background: none;
- border: 0;
- border-radius: $border-radius-base;
- transition: transform .15s cubic-bezier(.3, 0, .2, 2);
-
- &:hover {
- background-color: transparent;
- outline: 0;
- transform: scale(1.3);
- }
-
- &:focus,
- &:active {
- outline: 0;
- }
-
- .emoji-icon {
- display: inline-block;
- position: relative;
- top: 3px;
- }
-}
-
-.award-menu-holder {
- display: inline-block;
- position: relative;
-}
-
-.award-control {
- margin: 3px 5px 3px 0;
- padding: 5px 6px;
- outline: 0;
-
- &:hover,
- &.active,
- &:active {
- background-color: $row-hover;
- border-color: $row-hover-border;
- box-shadow: none;
- outline: 0;
- }
-
- &.btn {
- &:focus {
- outline: 0;
- }
- }
-
- &.is-loading {
- .award-control-icon-normal,
- .emoji-icon {
- display: none;
- }
-
- .award-control-icon-loading {
- display: block;
- }
- }
-
- .icon,
- .award-control-icon {
- float: left;
- margin-right: 5px;
- font-size: 19px;
- }
-
- .award-control-icon-loading {
- display: none;
- }
-
- .award-control-icon {
- color: $award-emoji-new-btn-icon-color;
- }
-}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 47a7e84b5c6..c735f104c20 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -98,7 +98,7 @@
.board-inner {
height: 100%;
font-size: $issue-boards-font-size;
- background: $background-color;
+ background: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
}
@@ -145,7 +145,7 @@
.board-blank-state {
height: calc(100% - 49px);
padding: $gl-padding;
- background-color: #fff;
+ background-color: $white-light;
}
.board-blank-state-list {
@@ -166,8 +166,12 @@
}
}
-.board-list {
+.board-list-component {
height: calc(100% - 49px);
+}
+
+.board-list {
+ height: 100%;
margin-bottom: 0;
padding: 5px;
list-style: none;
@@ -175,7 +179,7 @@
overflow-x: hidden;
&.is-smaller {
- height: calc(100% - 185px);
+ height: calc(100% - 136px);
}
}
@@ -187,9 +191,9 @@
.card {
position: relative;
padding: 10px $gl-padding;
- background: #fff;
+ background: $white-light;
border-radius: $border-radius-default;
- box-shadow: 0 1px 2px rgba(186, 186, 186, 0.5);
+ box-shadow: 0 1px 2px $issue-boards-card-shadow;
list-style: none;
&:not(:last-child) {
@@ -239,7 +243,7 @@
}
.issue-boards-search {
- width: 335px;
+ width: 290px;
.form-control {
display: inline-block;
@@ -249,7 +253,7 @@
.board-list-count {
padding: 10px 0;
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
font-size: 13px;
> .fa {
@@ -321,7 +325,6 @@
}
.issuable-header-text {
- width: 100%;
padding-right: 35px;
> strong {
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 6300ac9662f..66f7e7f97c8 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -1,7 +1,7 @@
.build-page {
pre.trace {
- background: #111;
- color: #fff;
+ background: $builds-trace-bg;
+ color: $white-light;
font-family: $monospace_font;
white-space: pre-wrap;
overflow: auto;
@@ -14,18 +14,10 @@
}
}
- .autoscroll-container {
- position: fixed;
- bottom: 20px;
- right: 20px;
- z-index: 100;
- }
-
.scroll-controls {
- &.affix-top {
- position: absolute;
- top: 10px;
- right: 25px;
+ .scroll-step {
+ width: 31px;
+ margin: 0 0 0 auto;
}
&.affix-bottom {
@@ -34,13 +26,13 @@
}
&.affix {
- right: 30px;
+ right: 25px;
bottom: 15px;
z-index: 1;
+ }
- @media (min-width: $screen-md-min) {
- right: 26%;
- }
+ &.sidebar-expanded {
+ right: #{$gutter_width + ($gl-padding * 2)};
}
a {
@@ -48,6 +40,19 @@
margin-bottom: 10px;
}
}
+
+ .environment-information {
+ background-color: $gray-light;
+ border: 1px solid $border-color;
+ padding: 12px $gl-padding;
+ border-radius: $border-radius-default;
+
+ svg {
+ position: relative;
+ top: 1px;
+ margin-right: 5px;
+ }
+ }
}
.build-header {
@@ -57,12 +62,9 @@
min-height: 58px;
align-items: center;
- .btn-inverted {
- @include btn-outline($white-light, $blue-normal, $blue-normal, $blue-light, $white-light, $blue-light);
- }
-
@media (max-width: $screen-sm-max) {
padding-right: 40px;
+ margin-top: 6px;
.btn-inverted {
display: none;
@@ -71,14 +73,14 @@
.header-content {
flex: 1;
- }
- a {
- color: $gl-gray;
+ a {
+ color: $gl-gray;
- &:hover {
- color: $gl-link-color;
- text-decoration: none;
+ &:hover {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
}
}
@@ -94,8 +96,8 @@
}
.build-trace {
- background: $ci-output-bg;
- color: $ci-text-color;
+ background: $black;
+ color: $gray-darkest;
white-space: pre;
overflow-x: auto;
font-size: 12px;
@@ -255,7 +257,7 @@
}
.build-light-text {
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
}
.build-gutter-toggle {
diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss
index 87c453a7a27..d1cd1e5d848 100644
--- a/app/assets/stylesheets/pages/ci_projects.scss
+++ b/app/assets/stylesheets/pages/ci_projects.scss
@@ -1,7 +1,7 @@
.ci-body {
.project-title {
margin: 0;
- color: #444;
+ color: $common-gray-dark;
font-size: 20px;
line-height: 1.5;
}
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index 47d3e72679b..bf656d0e28e 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -5,7 +5,7 @@
.commit-author,
.commit-committer {
display: block;
- color: #999;
+ color: $commit-committer-color;
font-weight: normal;
font-style: italic;
}
@@ -26,143 +26,12 @@
white-space: pre-wrap;
}
-.commit-info-row {
- margin-bottom: 10px;
- line-height: 24px;
- padding-top: 6px;
-
- &.commit-info-row-header {
- line-height: 34px;
- padding: 10px 0;
- margin-bottom: 0;
-
- @media (min-width: $screen-sm-min) {
- display: flex;
- align-items: center;
-
- .commit-meta {
- flex: 1;
- }
- }
-
- .commit-hash-full {
- @media (max-width: $screen-sm-max) {
- width: 80px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- display: inline-block;
- vertical-align: bottom;
- }
- }
-
- .commit-action-buttons {
- i {
- color: $gl-icon-color;
- font-size: 13px;
- margin-right: 3px;
- }
-
- @media (max-width: $screen-xs-max) {
- .dropdown {
- width: 100%;
- margin-top: 10px;
- }
-
- .dropdown-toggle {
- width: 100%;
- }
- }
- }
- }
-
- .avatar {
- @extend .avatar-inline;
- margin-left: 0;
-
- @media (min-width: $screen-sm-min) {
- margin-left: 4px;
- }
- }
-
- .commit-committer-link,
- .commit-author-link {
- color: $gl-gray;
- font-weight: bold;
- }
-
- .fa-clipboard {
- color: $dropdown-title-btn-color;
- }
-
- .commit-info {
- &.branches {
- margin-left: 8px;
- }
- }
-
- .ci-status-link {
-
- svg {
- position: relative;
- top: 2px;
- margin: 0 2px 0 3px;
- }
- }
-}
-
.js-details-expand {
&:hover {
text-decoration: none;
}
}
-.commit-info-widget {
- background: $background-color;
- color: $gl-gray;
- border: 1px solid $border-color;
- border-radius: $border-radius-default;
-
- .widget-row {
- padding: $gl-padding;
-
- &:not(:last-of-type) {
- border-bottom: 1px solid $widget-inner-border;
- }
-
- &.branch-info {
- .monospace,
- .commit-info {
- margin-left: 4px;
- }
- }
- }
-
- .icon-container {
- display: inline-block;
- margin-right: 8px;
-
- svg {
- position: relative;
- top: 2px;
- height: 16px;
- width: 16px;
- }
-
- &.commit-icon {
- svg {
- path {
- fill: $gl-text-color;
- }
- }
- }
- }
-
- .label.label-gray {
- background-color: $widget-expand-item;
- }
-}
-
.ci-status-link {
svg {
overflow: visible;
@@ -184,6 +53,17 @@
}
}
+.commit-hash-full {
+ @media (max-width: $screen-sm-max) {
+ width: 80px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+ vertical-align: bottom;
+ }
+}
+
.file-stats {
ul {
list-style: none;
@@ -233,17 +113,17 @@
overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
.max-width-marker {
width: 72ch;
- color: rgba(0, 0, 0, 0.0);
+ color: $commit-max-width-marker-color;
font-family: inherit;
left: $left;
height: 100%;
- border-right: 1px solid mix($input-border, white);
+ border-right: 1px solid mix($input-border, $white-light);
position: absolute;
z-index: 1;
}
> textarea {
- background-color: rgba(0, 0, 0, 0.0);
+ background-color: $commit-message-text-area-bg;
font-family: inherit;
padding-left: $left;
position: relative;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 98a84351a3d..e76e1a73b25 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -7,9 +7,9 @@
.commit-header {
padding: 5px 10px;
- background-color: $background-color;
- border-top: 1px solid #eee;
- border-bottom: 1px solid #eee;
+ background-color: $gray-light;
+ border-top: 1px solid $gray-darker;
+ border-bottom: 1px solid $gray-darker;
font-size: 14px;
&:first-child {
@@ -38,7 +38,7 @@
.text-expander {
display: inline-block;
background: $gray-light;
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
padding: 0 5px;
cursor: pointer;
border: 1px solid $border-gray-dark;
@@ -62,6 +62,8 @@
.ci-status-link {
display: inline-block;
+ position: relative;
+ top: 1px;
}
.btn-clipboard,
@@ -82,7 +84,8 @@
font-weight: 600;
}
-.commit {
+.commit,
+.generic_commit_status {
padding: 10px 0;
position: relative;
@@ -91,7 +94,7 @@
}
&:not(:last-child) {
- border-bottom: 1px solid #eee;
+ border-bottom: 1px solid $gray-darker;
}
a,
@@ -100,7 +103,6 @@
vertical-align: baseline;
}
-
.avatar {
margin-left: -46px;
}
@@ -115,7 +117,7 @@
.commit-row-description {
font-size: 14px;
- border-left: 1px solid $btn-gray-hover;
+ border-left: 1px solid $white-normal;
padding: 10px 15px;
margin: 10px 0;
background: $gray-light;
@@ -172,7 +174,7 @@
height: 14px;
width: 14px;
vertical-align: middle;
- fill: $table-text-gray;
+ fill: $gl-gray-light;
}
}
@@ -199,7 +201,7 @@
.bar {
position: absolute;
height: 4px;
- background-color: #ccc;
+ background-color: $divergence-graph-bar-bg;
}
.bar-behind {
@@ -216,7 +218,7 @@
padding-top: 6px;
padding-bottom: 0;
font-size: 12px;
- color: #333;
+ color: $gl-title-color;
display: block;
}
@@ -237,6 +239,6 @@
height: 18px;
margin: 5px 0 0;
float: left;
- background-color: #ccc;
+ background-color: $divergence-graph-separator-bg;
}
}
diff --git a/app/assets/stylesheets/pages/confirmation.scss b/app/assets/stylesheets/pages/confirmation.scss
deleted file mode 100644
index 81e5cee240d..00000000000
--- a/app/assets/stylesheets/pages/confirmation.scss
+++ /dev/null
@@ -1,32 +0,0 @@
-.well-confirmation {
- margin-bottom: 20px;
- border-bottom: 1px solid #eee;
-
- > h1,
- h2,
- h3,
- h4,
- h5,
- h6 {
- font-weight: 400;
- }
-
- .lead {
- margin-bottom: 20px;
- }
-
- ul,
- ol {
- padding-left: 0;
- }
-
- li {
- list-style-type: none;
- }
-}
-
-.confirmation-content {
- a {
- color: $md-link-color;
- }
-}
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 572e1e7d558..5aef31724e1 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -1,16 +1,59 @@
#cycle-analytics {
+ max-width: 1000px;
margin: 24px auto 0;
- max-width: 800px;
position: relative;
- .panel {
+ .col-headers {
+ ul {
+ margin: 0;
+ padding: 0;
+ @include clearfix;
+ }
+
+ li {
+ display: inline-block;
+ float: left;
+ line-height: 50px;
+ width: 20%;
+ }
+
+
+ .fa {
+ color: $cycle-analytics-light-gray;
+ }
+
+ .stage-header {
+ width: 28%;
+ padding-left: $gl-padding;
+ }
+ .median-header {
+ width: 12%;
+ }
+
+ .event-header {
+ width: 45%;
+ padding-left: $gl-padding;
+ }
+
+ .total-time-header {
+ width: 15%;
+ text-align: right;
+ padding-right: $gl-padding;
+ }
+
+ .stage-name {
+ font-weight: 600;
+ }
+ }
+
+ .panel {
.content-block {
padding: 24px 0;
border-bottom: none;
position: relative;
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
padding: 6px 0 24px;
}
}
@@ -18,7 +61,7 @@
.column {
text-align: center;
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
padding: 15px 0;
}
@@ -35,23 +78,20 @@
}
&:last-child {
- text-align: right;
-
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
text-align: center;
}
}
}
+ }
- .dropdown {
- top: 13px;
- }
+ .js-ca-dropdown {
+ top: $gl-padding-top;
}
.bordered-box {
border: 1px solid $border-color;
border-radius: $border-radius-default;
-
}
.content-list {
@@ -69,7 +109,7 @@
&.title {
line-height: 19px;
- font-size: 15px;
+ font-size: 14px;
font-weight: 600;
color: $gl-title-color;
}
@@ -101,9 +141,9 @@
.dismiss-icon {
position: absolute;
- right: $cycle-analytics-box-padding;
+ right: $cycle-analytics-dismiss-icon-color;
cursor: pointer;
- color: #b2b2b2;
+ color: $cycle-analytics-dismiss-icon-color;
}
.svg-container {
@@ -116,7 +156,7 @@
}
.inner-content {
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
padding: 0 28px;
text-align: center;
}
@@ -141,4 +181,303 @@
margin-top: 36px;
}
+ .stage-panel-body {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ .stage-nav,
+ .stage-entries {
+ display: flex;
+ vertical-align: top;
+ font-size: $gl-font-size;
+ }
+
+ .stage-nav {
+ width: 40%;
+ margin-bottom: 0;
+
+ ul {
+ padding: 0;
+ margin: 0;
+ width: 100%;
+ }
+
+ li {
+ list-style-type: none;
+ @include clearfix;
+ }
+
+ .stage-nav-item {
+ display: block;
+ line-height: 65px;
+ border-top: 1px solid transparent;
+ border-bottom: 1px solid transparent;
+ border-right: 1px solid $border-color;
+ background-color: $gray-light;
+ cursor: default;
+
+ &.active {
+ background-color: transparent;
+ border-right-color: transparent;
+ border-top-color: $border-color;
+ border-bottom-color: $border-color;
+ box-shadow: inset 2px 0 0 0 $active-item-blue;
+
+ .stage-name {
+ font-weight: 600;
+ }
+ }
+
+ &:hover:not(.active) {
+ background-color: $gray-lightest;
+ box-shadow: inset 2px 0 0 0 $border-color;
+ cursor: pointer;
+ }
+
+ &:first-child {
+ border-top: none;
+ }
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .stage-nav-item-cell {
+ float: left;
+
+ &.stage-name {
+ width: 70%;
+ }
+
+ &.stage-median {
+ width: 30%;
+ }
+ }
+
+ .stage-name {
+ padding-left: 16px;
+ }
+
+ .stage-empty,
+ .not-available {
+ color: $gl-text-color-light;
+ }
+ }
+ }
+
+ .stage-panel-container {
+ width: 100%;
+ overflow: auto;
+ }
+
+ .stage-panel {
+ min-width: 968px;
+
+ .panel-heading {
+ padding: 0;
+ background-color: transparent;
+ }
+
+ .events-description {
+ line-height: 65px;
+ padding-left: $gl-padding;
+ }
+ }
+
+ .stage-events {
+ width: 60%;
+ overflow: scroll;
+ height: 467px;
+ }
+
+ .stage-event-list {
+ margin: 0;
+ padding: 0;
+ }
+
+ .stage-event-item {
+ list-style-type: none;
+ padding: 0 0 $gl-padding;
+ margin: 0 $gl-padding $gl-padding;
+ border-bottom: 1px solid $gray-darker;
+ @include clearfix;
+
+ &:last-child {
+ border-bottom: none;
+ margin-bottom: 0;
+ }
+
+ .item-details,
+ .item-time {
+ float: left;
+ }
+
+ .item-details {
+ width: 75%;
+ }
+
+ .item-title {
+ margin: 0 0 2px;
+
+ &.issue-title,
+ &.commit-title,
+ &.merge-merquest-title {
+ max-width: 100%;
+ display: block;
+ @include text-overflow();
+
+ a {
+ color: $gl-dark-link-color;
+ }
+ }
+ }
+
+ .item-time {
+ width: 25%;
+ text-align: right;
+ }
+
+ .total-time {
+ font-size: $cycle-analytics-big-font;
+ color: $cycle-analytics-dark-text;
+
+ span {
+ color: $gl-text-color;
+ font-size: $gl-font-size;
+ }
+ }
+
+ .issue-date,
+ .build-date {
+ color: $gl-text-color;
+ }
+
+ .issue-link,
+ .commit-author-link,
+ .issue-author-link {
+ color: $gl-dark-link-color;
+ }
+
+ // Custom CSS for components
+ .item-conmmit-component {
+ .commit-icon {
+ position: relative;
+ top: 3px;
+ left: 1px;
+ display: inline-block;
+
+ svg {
+ float: left;
+ }
+ }
+ }
+
+ .merge-request-branch {
+ a {
+ max-width: 180px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ display: inline-block;
+ vertical-align: bottom;
+ }
+ }
+ }
+
+ // Custom Styles for stage items
+ .item-build-component {
+
+ .item-title {
+ .icon-build-status {
+ float: left;
+ margin-right: 5px;
+ position: relative;
+ top: 2px;
+ }
+
+ .item-build-name {
+ color: $gl-title-color;
+ }
+
+ .pipeline-id {
+ color: $gl-title-color;
+ padding: 0 3px 0 0;
+ }
+
+ .branch-name {
+ color: $black;
+ display: inline-block;
+ max-width: 180px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ line-height: 1.3;
+ vertical-align: top;
+ }
+
+ .short-sha {
+ color: $gl-link-color;
+ line-height: 1.3;
+ vertical-align: top;
+ font-weight: normal;
+ }
+
+ .fa {
+ color: $gl-text-color-light;
+ font-size: $code_font_size;
+ }
+ }
+ }
+
+ .empty-stage,
+ .no-access-stage {
+ text-align: center;
+ width: 75%;
+ margin: 0 auto;
+ padding-top: 130px;
+ color: $gl-text-color-light;
+
+ h4 {
+ color: $gl-text-color;
+ }
+ }
+
+ .empty-stage {
+ .icon-no-data {
+ height: 36px;
+ width: 78px;
+ display: inline-block;
+ margin-bottom: 20px;
+ }
+ }
+
+ .no-access-stage {
+ .icon-lock {
+ height: 36px;
+ width: 78px;
+ display: inline-block;
+ margin-bottom: 20px;
+ }
+ }
+}
+
+.cycle-analytics-overview {
+ padding-top: 100px;
+
+ .overview-details {
+ display: flex;
+ align-items: center;
+ }
+
+ .overview-image {
+ text-align: right;
+ }
+
+ .overview-icon {
+ svg {
+ width: 365px;
+ height: 227px;
+ }
+ }
}
diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss
deleted file mode 100644
index 016bab104eb..00000000000
--- a/app/assets/stylesheets/pages/dashboard.scss
+++ /dev/null
@@ -1,43 +0,0 @@
-.dashboard {
- .side {
- .panel {
- .panel-heading {
- background: $background-color;
- border-top-left-radius: 0;
- }
-
- border-top-left-radius: 0;
- }
- }
-}
-
-.dashboard-search-filter {
- padding: 5px;
-
- .search-text-input {
- float: left;
- @extend .col-md-2;
- }
-
- .btn {
- margin-left: 5px;
- float: left;
- }
-}
-
-.project-access-icon {
- margin-left: 10px;
- float: left;
- margin-right: 15px;
- margin-bottom: 15px;
-
- i {
- color: #888;
- }
-}
-
-.dash-project-access-icon {
- float: left;
- margin-right: 5px;
- width: 16px;
-}
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 0f0c0abe7ae..80baebd5ea3 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -1,16 +1,16 @@
.detail-page-header {
padding: $gl-padding-top 0;
border-bottom: 1px solid $border-color;
- color: #5c5d5e;
+ color: $gl-text-color-dark;
font-size: 16px;
line-height: 34px;
.author {
- color: #5c5d5e;
+ color: $gl-text-color-dark;
}
.identifier {
- color: #5c5d5e;
+ color: $gl-text-color-dark;
}
.issue_created_ago,
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index fde138c874d..f30795fd2c2 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -11,10 +11,10 @@
.diff-header {
position: relative;
- background: $background-color;
+ background: $gray-light;
border-bottom: 1px solid $border-color;
padding: 10px 16px;
- color: #555;
+ color: $gl-diff-text-color;
z-index: 10;
border-radius: 3px 3px 0 0;
@@ -24,7 +24,7 @@
display: block;
.file-mode {
- color: #777;
+ color: $file-mode-changed;
}
}
@@ -38,7 +38,7 @@
cursor: pointer;
&:hover {
- background-color: $dark-background-color;
+ background-color: $gray-normal;
}
.diff-toggle-caret {
@@ -49,8 +49,8 @@
.diff-content {
overflow: auto;
overflow-y: hidden;
- background: #fff;
- color: #333;
+ background: $white-light;
+ color: $gl-title-color;
border-radius: 0 0 3px 3px;
.unfold {
@@ -59,7 +59,7 @@
.file-mode-changed {
padding: 10px;
- color: #777;
+ color: $file-mode-changed;
}
.suppressed-container {
@@ -92,20 +92,6 @@
&.noteable_line {
position: relative;
-
- &.old {
- &::before {
- content: '-';
- position: absolute;
- }
- }
-
- &.new {
- &::before {
- content: '+';
- position: absolute;
- }
- }
}
span {
@@ -151,8 +137,9 @@
.line_content {
display: block;
margin: 0;
- padding: 0 0.5em;
+ padding: 0 1.5em;
border: none;
+ position: relative;
&.parallel {
display: table-cell;
@@ -161,6 +148,22 @@
word-break: break-all;
}
}
+
+ &.old {
+ &::before {
+ content: '-';
+ position: absolute;
+ left: 0.5em;
+ }
+ }
+
+ &.new {
+ &::before {
+ content: '+';
+ position: absolute;
+ left: 0.5em;
+ }
+ }
}
.text-file.diff-wrap-lines table .line_holder td span {
@@ -169,7 +172,7 @@
}
.image {
- background: #ddd;
+ background: $diff-image-bg;
text-align: center;
padding: 30px;
@@ -179,13 +182,13 @@
.frame {
display: inline-block;
- background-color: #fff;
+ background-color: $white-light;
line-height: 0;
img {
- border: 1px solid #fff;
- background-image: linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5 100%),
- linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5 100%);
+ border: 1px solid $white-light;
+ background-image: linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%),
+ linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%);
background-size: 10px 10px;
background-position: 0 0, 5px 5px;
max-width: 100%;
@@ -203,7 +206,7 @@
.image-info {
font-size: 12px;
margin: 5px 0 0;
- color: grey;
+ color: $diff-image-info-color;
}
.view.swipe {
@@ -217,7 +220,7 @@
.swipe-wrap {
overflow: hidden;
- border-left: 1px solid #999;
+ border-left: 1px solid $diff-swipe-border;
position: absolute;
display: block;
top: 13px;
@@ -347,7 +350,7 @@
.view-modes {
padding: 10px;
text-align: center;
- background: #eee;
+ background: $gray-darker;
ul,
li {
@@ -358,8 +361,8 @@
}
li {
- color: grey;
- border-left: 1px solid #c1c1c1;
+ color: $diff-view-modes-color;
+ border-left: 1px solid $diff-view-modes-border;
padding: 0 12px 0 16px;
cursor: pointer;
@@ -377,7 +380,7 @@
}
cursor: default;
- color: #333;
+ color: $gl-title-color;
}
&.disabled {
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index ac968618c79..4af267403d8 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -10,14 +10,14 @@
}
.ace_gutter-cell {
- background-color: $background-color;
+ background-color: $gray-light;
}
.cancel-btn {
- color: #b94a48;
+ color: $editor-cancel-color;
&:hover {
- color: #b94a48;
+ color: $editor-cancel-color;
}
}
@@ -34,7 +34,7 @@
}
.editor-ref {
- background: $background-color;
+ background: $gray-light;
padding-right: $gl-padding;
border-right: 1px solid $border-color;
display: block;
@@ -51,8 +51,16 @@
.new-file-name {
display: inline-block;
- width: 450px;
+ max-width: 450px;
float: left;
+
+ @media(max-width: $screen-md-max) {
+ width: 280px;
+ }
+
+ @media(max-width: $screen-sm-max) {
+ width: 180px;
+ }
}
.file-buttons {
@@ -116,3 +124,42 @@
}
}
}
+
+@media(max-width: $screen-xs-max){
+ .file-editor {
+ .file-title {
+ .pull-right {
+ height: auto;
+ }
+ }
+
+ .new-file-name {
+ max-width: none;
+ width: 100%;
+ margin-bottom: 3px;
+ }
+
+ .file-buttons {
+ display: block;
+ width: 100%;
+ margin-bottom: 10px;
+
+ .soft-wrap-toggle {
+ width: 100%;
+ margin: 3px 0;
+ }
+
+ .encoding-selector,
+ .license-selector,
+ .gitignore-selector,
+ .gitlab-ci-yml-selector {
+ display: block;
+ margin: 3px 0;
+
+ button {
+ width: 100%;
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/emojis.scss b/app/assets/stylesheets/pages/emojis.scss
deleted file mode 100644
index f17797b2381..00000000000
--- a/app/assets/stylesheets/pages/emojis.scss
+++ /dev/null
@@ -1,1809 +0,0 @@
-.emoji-0023-20E3 { background-position: 0 0px; }
-.emoji-002A-20E3 { background-position: -20px 0; }
-.emoji-0030-20E3 { background-position: 0 -20px; }
-.emoji-0031-20E3 { background-position: -20px -20px; }
-.emoji-0032-20E3 { background-position: -40px 0; }
-.emoji-0033-20E3 { background-position: -40px -20px; }
-.emoji-0034-20E3 { background-position: 0 -40px; }
-.emoji-0035-20E3 { background-position: -20px -40px; }
-.emoji-0036-20E3 { background-position: -40px -40px; }
-.emoji-0037-20E3 { background-position: -60px 0; }
-.emoji-0038-20E3 { background-position: -60px -20px; }
-.emoji-0039-20E3 { background-position: -60px -40px; }
-.emoji-00A9 { background-position: 0 -60px; }
-.emoji-00AE { background-position: -20px -60px; }
-.emoji-1F004 { background-position: -40px -60px; }
-.emoji-1F0CF { background-position: -60px -60px; }
-.emoji-1F170 { background-position: -80px 0; }
-.emoji-1F171 { background-position: -80px -20px; }
-.emoji-1F17E { background-position: -80px -40px; }
-.emoji-1F17F { background-position: -80px -60px; }
-.emoji-1F18E { background-position: 0 -80px; }
-.emoji-1F191 { background-position: -20px -80px; }
-.emoji-1F192 { background-position: -40px -80px; }
-.emoji-1F193 { background-position: -60px -80px; }
-.emoji-1F194 { background-position: -80px -80px; }
-.emoji-1F195 { background-position: -100px 0; }
-.emoji-1F196 { background-position: -100px -20px; }
-.emoji-1F197 { background-position: -100px -40px; }
-.emoji-1F198 { background-position: -100px -60px; }
-.emoji-1F199 { background-position: -100px -80px; }
-.emoji-1F19A { background-position: 0 -100px; }
-.emoji-1F1E6-1F1E8 { background-position: -20px -100px; }
-.emoji-1F1E6-1F1E9 { background-position: -40px -100px; }
-.emoji-1F1E6-1F1EA { background-position: -60px -100px; }
-.emoji-1F1E6-1F1EB { background-position: -80px -100px; }
-.emoji-1F1E6-1F1EC { background-position: -100px -100px; }
-.emoji-1F1E6-1F1EE { background-position: -120px 0; }
-.emoji-1F1E6-1F1F1 { background-position: -120px -20px; }
-.emoji-1F1E6-1F1F2 { background-position: -120px -40px; }
-.emoji-1F1E6-1F1F4 { background-position: -120px -60px; }
-.emoji-1F1E6-1F1F6 { background-position: -120px -80px; }
-.emoji-1F1E6-1F1F7 { background-position: -120px -100px; }
-.emoji-1F1E6-1F1F8 { background-position: 0 -120px; }
-.emoji-1F1E6-1F1F9 { background-position: -20px -120px; }
-.emoji-1F1E6-1F1FA { background-position: -40px -120px; }
-.emoji-1F1E6-1F1FC { background-position: -60px -120px; }
-.emoji-1F1E6-1F1FD { background-position: -80px -120px; }
-.emoji-1F1E6-1F1FF { background-position: -100px -120px; }
-.emoji-1F1E7-1F1E6 { background-position: -120px -120px; }
-.emoji-1F1E7-1F1E7 { background-position: -140px 0; }
-.emoji-1F1E7-1F1E9 { background-position: -140px -20px; }
-.emoji-1F1E7-1F1EA { background-position: -140px -40px; }
-.emoji-1F1E7-1F1EB { background-position: -140px -60px; }
-.emoji-1F1E7-1F1EC { background-position: -140px -80px; }
-.emoji-1F1E7-1F1ED { background-position: -140px -100px; }
-.emoji-1F1E7-1F1EE { background-position: -140px -120px; }
-.emoji-1F1E7-1F1EF { background-position: 0 -140px; }
-.emoji-1F1E7-1F1F1 { background-position: -20px -140px; }
-.emoji-1F1E7-1F1F2 { background-position: -40px -140px; }
-.emoji-1F1E7-1F1F3 { background-position: -60px -140px; }
-.emoji-1F1E7-1F1F4 { background-position: -80px -140px; }
-.emoji-1F1E7-1F1F6 { background-position: -100px -140px; }
-.emoji-1F1E7-1F1F7 { background-position: -120px -140px; }
-.emoji-1F1E7-1F1F8 { background-position: -140px -140px; }
-.emoji-1F1E7-1F1F9 { background-position: -160px 0; }
-.emoji-1F1E7-1F1FB { background-position: -160px -20px; }
-.emoji-1F1E7-1F1FC { background-position: -160px -40px; }
-.emoji-1F1E7-1F1FE { background-position: -160px -60px; }
-.emoji-1F1E7-1F1FF { background-position: -160px -80px; }
-.emoji-1F1E8-1F1E6 { background-position: -160px -100px; }
-.emoji-1F1E8-1F1E8 { background-position: -160px -120px; }
-.emoji-1F1E8-1F1E9 { background-position: -160px -140px; }
-.emoji-1F1E8-1F1EB { background-position: 0 -160px; }
-.emoji-1F1E8-1F1EC { background-position: -20px -160px; }
-.emoji-1F1E8-1F1ED { background-position: -40px -160px; }
-.emoji-1F1E8-1F1EE { background-position: -60px -160px; }
-.emoji-1F1E8-1F1F0 { background-position: -80px -160px; }
-.emoji-1F1E8-1F1F1 { background-position: -100px -160px; }
-.emoji-1F1E8-1F1F2 { background-position: -120px -160px; }
-.emoji-1F1E8-1F1F3 { background-position: -140px -160px; }
-.emoji-1F1E8-1F1F4 { background-position: -160px -160px; }
-.emoji-1F1E8-1F1F5 { background-position: -180px 0; }
-.emoji-1F1E8-1F1F7 { background-position: -180px -20px; }
-.emoji-1F1E8-1F1FA { background-position: -180px -40px; }
-.emoji-1F1E8-1F1FB { background-position: -180px -60px; }
-.emoji-1F1E8-1F1FC { background-position: -180px -80px; }
-.emoji-1F1E8-1F1FD { background-position: -180px -100px; }
-.emoji-1F1E8-1F1FE { background-position: -180px -120px; }
-.emoji-1F1E8-1F1FF { background-position: -180px -140px; }
-.emoji-1F1E9-1F1EA { background-position: -180px -160px; }
-.emoji-1F1E9-1F1EC { background-position: 0 -180px; }
-.emoji-1F1E9-1F1EF { background-position: -20px -180px; }
-.emoji-1F1E9-1F1F0 { background-position: -40px -180px; }
-.emoji-1F1E9-1F1F2 { background-position: -60px -180px; }
-.emoji-1F1E9-1F1F4 { background-position: -80px -180px; }
-.emoji-1F1E9-1F1FF { background-position: -100px -180px; }
-.emoji-1F1EA-1F1E6 { background-position: -120px -180px; }
-.emoji-1F1EA-1F1E8 { background-position: -140px -180px; }
-.emoji-1F1EA-1F1EA { background-position: -160px -180px; }
-.emoji-1F1EA-1F1EC { background-position: -180px -180px; }
-.emoji-1F1EA-1F1ED { background-position: -200px 0; }
-.emoji-1F1EA-1F1F7 { background-position: -200px -20px; }
-.emoji-1F1EA-1F1F8 { background-position: -200px -40px; }
-.emoji-1F1EA-1F1F9 { background-position: -200px -60px; }
-.emoji-1F1EA-1F1FA { background-position: -200px -80px; }
-.emoji-1F1EB-1F1EE { background-position: -200px -100px; }
-.emoji-1F1EB-1F1EF { background-position: -200px -120px; }
-.emoji-1F1EB-1F1F0 { background-position: -200px -140px; }
-.emoji-1F1EB-1F1F2 { background-position: -200px -160px; }
-.emoji-1F1EB-1F1F4 { background-position: -200px -180px; }
-.emoji-1F1EB-1F1F7 { background-position: 0 -200px; }
-.emoji-1F1EC-1F1E6 { background-position: -20px -200px; }
-.emoji-1F1EC-1F1E7 { background-position: -40px -200px; }
-.emoji-1F1EC-1F1E9 { background-position: -60px -200px; }
-.emoji-1F1EC-1F1EA { background-position: -80px -200px; }
-.emoji-1F1EC-1F1EB { background-position: -100px -200px; }
-.emoji-1F1EC-1F1EC { background-position: -120px -200px; }
-.emoji-1F1EC-1F1ED { background-position: -140px -200px; }
-.emoji-1F1EC-1F1EE { background-position: -160px -200px; }
-.emoji-1F1EC-1F1F1 { background-position: -180px -200px; }
-.emoji-1F1EC-1F1F2 { background-position: -200px -200px; }
-.emoji-1F1EC-1F1F3 { background-position: -220px 0; }
-.emoji-1F1EC-1F1F5 { background-position: -220px -20px; }
-.emoji-1F1EC-1F1F6 { background-position: -220px -40px; }
-.emoji-1F1EC-1F1F7 { background-position: -220px -60px; }
-.emoji-1F1EC-1F1F8 { background-position: -220px -80px; }
-.emoji-1F1EC-1F1F9 { background-position: -220px -100px; }
-.emoji-1F1EC-1F1FA { background-position: -220px -120px; }
-.emoji-1F1EC-1F1FC { background-position: -220px -140px; }
-.emoji-1F1EC-1F1FE { background-position: -220px -160px; }
-.emoji-1F1ED-1F1F0 { background-position: -220px -180px; }
-.emoji-1F1ED-1F1F2 { background-position: -220px -200px; }
-.emoji-1F1ED-1F1F3 { background-position: 0 -220px; }
-.emoji-1F1ED-1F1F7 { background-position: -20px -220px; }
-.emoji-1F1ED-1F1F9 { background-position: -40px -220px; }
-.emoji-1F1ED-1F1FA { background-position: -60px -220px; }
-.emoji-1F1EE-1F1E8 { background-position: -80px -220px; }
-.emoji-1F1EE-1F1E9 { background-position: -100px -220px; }
-.emoji-1F1EE-1F1EA { background-position: -120px -220px; }
-.emoji-1F1EE-1F1F1 { background-position: -140px -220px; }
-.emoji-1F1EE-1F1F2 { background-position: -160px -220px; }
-.emoji-1F1EE-1F1F3 { background-position: -180px -220px; }
-.emoji-1F1EE-1F1F4 { background-position: -200px -220px; }
-.emoji-1F1EE-1F1F6 { background-position: -220px -220px; }
-.emoji-1F1EE-1F1F7 { background-position: -240px 0; }
-.emoji-1F1EE-1F1F8 { background-position: -240px -20px; }
-.emoji-1F1EE-1F1F9 { background-position: -240px -40px; }
-.emoji-1F1EF-1F1EA { background-position: -240px -60px; }
-.emoji-1F1EF-1F1F2 { background-position: -240px -80px; }
-.emoji-1F1EF-1F1F4 { background-position: -240px -100px; }
-.emoji-1F1EF-1F1F5 { background-position: -240px -120px; }
-.emoji-1F1F0-1F1EA { background-position: -240px -140px; }
-.emoji-1F1F0-1F1EC { background-position: -240px -160px; }
-.emoji-1F1F0-1F1ED { background-position: -240px -180px; }
-.emoji-1F1F0-1F1EE { background-position: -240px -200px; }
-.emoji-1F1F0-1F1F2 { background-position: -240px -220px; }
-.emoji-1F1F0-1F1F3 { background-position: 0 -240px; }
-.emoji-1F1F0-1F1F5 { background-position: -20px -240px; }
-.emoji-1F1F0-1F1F7 { background-position: -40px -240px; }
-.emoji-1F1F0-1F1FC { background-position: -60px -240px; }
-.emoji-1F1F0-1F1FE { background-position: -80px -240px; }
-.emoji-1F1F0-1F1FF { background-position: -100px -240px; }
-.emoji-1F1F1-1F1E6 { background-position: -120px -240px; }
-.emoji-1F1F1-1F1E7 { background-position: -140px -240px; }
-.emoji-1F1F1-1F1E8 { background-position: -160px -240px; }
-.emoji-1F1F1-1F1EE { background-position: -180px -240px; }
-.emoji-1F1F1-1F1F0 { background-position: -200px -240px; }
-.emoji-1F1F1-1F1F7 { background-position: -220px -240px; }
-.emoji-1F1F1-1F1F8 { background-position: -240px -240px; }
-.emoji-1F1F1-1F1F9 { background-position: -260px 0; }
-.emoji-1F1F1-1F1FA { background-position: -260px -20px; }
-.emoji-1F1F1-1F1FB { background-position: -260px -40px; }
-.emoji-1F1F1-1F1FE { background-position: -260px -60px; }
-.emoji-1F1F2-1F1E6 { background-position: -260px -80px; }
-.emoji-1F1F2-1F1E8 { background-position: -260px -100px; }
-.emoji-1F1F2-1F1E9 { background-position: -260px -120px; }
-.emoji-1F1F2-1F1EA { background-position: -260px -140px; }
-.emoji-1F1F2-1F1EB { background-position: -260px -160px; }
-.emoji-1F1F2-1F1EC { background-position: -260px -180px; }
-.emoji-1F1F2-1F1ED { background-position: -260px -200px; }
-.emoji-1F1F2-1F1F0 { background-position: -260px -220px; }
-.emoji-1F1F2-1F1F1 { background-position: -260px -240px; }
-.emoji-1F1F2-1F1F2 { background-position: 0 -260px; }
-.emoji-1F1F2-1F1F3 { background-position: -20px -260px; }
-.emoji-1F1F2-1F1F4 { background-position: -40px -260px; }
-.emoji-1F1F2-1F1F5 { background-position: -60px -260px; }
-.emoji-1F1F2-1F1F6 { background-position: -80px -260px; }
-.emoji-1F1F2-1F1F7 { background-position: -100px -260px; }
-.emoji-1F1F2-1F1F8 { background-position: -120px -260px; }
-.emoji-1F1F2-1F1F9 { background-position: -140px -260px; }
-.emoji-1F1F2-1F1FA { background-position: -160px -260px; }
-.emoji-1F1F2-1F1FB { background-position: -180px -260px; }
-.emoji-1F1F2-1F1FC { background-position: -200px -260px; }
-.emoji-1F1F2-1F1FD { background-position: -220px -260px; }
-.emoji-1F1F2-1F1FE { background-position: -240px -260px; }
-.emoji-1F1F2-1F1FF { background-position: -260px -260px; }
-.emoji-1F1F3-1F1E6 { background-position: -280px 0; }
-.emoji-1F1F3-1F1E8 { background-position: -280px -20px; }
-.emoji-1F1F3-1F1EA { background-position: -280px -40px; }
-.emoji-1F1F3-1F1EB { background-position: -280px -60px; }
-.emoji-1F1F3-1F1EC { background-position: -280px -80px; }
-.emoji-1F1F3-1F1EE { background-position: -280px -100px; }
-.emoji-1F1F3-1F1F1 { background-position: -280px -120px; }
-.emoji-1F1F3-1F1F4 { background-position: -280px -140px; }
-.emoji-1F1F3-1F1F5 { background-position: -280px -160px; }
-.emoji-1F1F3-1F1F7 { background-position: -280px -180px; }
-.emoji-1F1F3-1F1FA { background-position: -280px -200px; }
-.emoji-1F1F3-1F1FF { background-position: -280px -220px; }
-.emoji-1F1F4-1F1F2 { background-position: -280px -240px; }
-.emoji-1F1F5-1F1E6 { background-position: -280px -260px; }
-.emoji-1F1F5-1F1EA { background-position: 0 -280px; }
-.emoji-1F1F5-1F1EB { background-position: -20px -280px; }
-.emoji-1F1F5-1F1EC { background-position: -40px -280px; }
-.emoji-1F1F5-1F1ED { background-position: -60px -280px; }
-.emoji-1F1F5-1F1F0 { background-position: -80px -280px; }
-.emoji-1F1F5-1F1F1 { background-position: -100px -280px; }
-.emoji-1F1F5-1F1F2 { background-position: -120px -280px; }
-.emoji-1F1F5-1F1F3 { background-position: -140px -280px; }
-.emoji-1F1F5-1F1F7 { background-position: -160px -280px; }
-.emoji-1F1F5-1F1F8 { background-position: -180px -280px; }
-.emoji-1F1F5-1F1F9 { background-position: -200px -280px; }
-.emoji-1F1F5-1F1FC { background-position: -220px -280px; }
-.emoji-1F1F5-1F1FE { background-position: -240px -280px; }
-.emoji-1F1F6-1F1E6 { background-position: -260px -280px; }
-.emoji-1F1F7-1F1EA { background-position: -280px -280px; }
-.emoji-1F1F7-1F1F4 { background-position: -300px 0; }
-.emoji-1F1F7-1F1F8 { background-position: -300px -20px; }
-.emoji-1F1F7-1F1FA { background-position: -300px -40px; }
-.emoji-1F1F7-1F1FC { background-position: -300px -60px; }
-.emoji-1F1F8-1F1E6 { background-position: -300px -80px; }
-.emoji-1F1F8-1F1E7 { background-position: -300px -100px; }
-.emoji-1F1F8-1F1E8 { background-position: -300px -120px; }
-.emoji-1F1F8-1F1E9 { background-position: -300px -140px; }
-.emoji-1F1F8-1F1EA { background-position: -300px -160px; }
-.emoji-1F1F8-1F1EC { background-position: -300px -180px; }
-.emoji-1F1F8-1F1ED { background-position: -300px -200px; }
-.emoji-1F1F8-1F1EE { background-position: -300px -220px; }
-.emoji-1F1F8-1F1EF { background-position: -300px -240px; }
-.emoji-1F1F8-1F1F0 { background-position: -300px -260px; }
-.emoji-1F1F8-1F1F1 { background-position: -300px -280px; }
-.emoji-1F1F8-1F1F2 { background-position: 0 -300px; }
-.emoji-1F1F8-1F1F3 { background-position: -20px -300px; }
-.emoji-1F1F8-1F1F4 { background-position: -40px -300px; }
-.emoji-1F1F8-1F1F7 { background-position: -60px -300px; }
-.emoji-1F1F8-1F1F8 { background-position: -80px -300px; }
-.emoji-1F1F8-1F1F9 { background-position: -100px -300px; }
-.emoji-1F1F8-1F1FB { background-position: -120px -300px; }
-.emoji-1F1F8-1F1FD { background-position: -140px -300px; }
-.emoji-1F1F8-1F1FE { background-position: -160px -300px; }
-.emoji-1F1F8-1F1FF { background-position: -180px -300px; }
-.emoji-1F1F9-1F1E6 { background-position: -200px -300px; }
-.emoji-1F1F9-1F1E8 { background-position: -220px -300px; }
-.emoji-1F1F9-1F1E9 { background-position: -240px -300px; }
-.emoji-1F1F9-1F1EB { background-position: -260px -300px; }
-.emoji-1F1F9-1F1EC { background-position: -280px -300px; }
-.emoji-1F1F9-1F1ED { background-position: -300px -300px; }
-.emoji-1F1F9-1F1EF { background-position: -320px 0; }
-.emoji-1F1F9-1F1F0 { background-position: -320px -20px; }
-.emoji-1F1F9-1F1F1 { background-position: -320px -40px; }
-.emoji-1F1F9-1F1F2 { background-position: -320px -60px; }
-.emoji-1F1F9-1F1F3 { background-position: -320px -80px; }
-.emoji-1F1F9-1F1F4 { background-position: -320px -100px; }
-.emoji-1F1F9-1F1F7 { background-position: -320px -120px; }
-.emoji-1F1F9-1F1F9 { background-position: -320px -140px; }
-.emoji-1F1F9-1F1FB { background-position: -320px -160px; }
-.emoji-1F1F9-1F1FC { background-position: -320px -180px; }
-.emoji-1F1F9-1F1FF { background-position: -320px -200px; }
-.emoji-1F1FA-1F1E6 { background-position: -320px -220px; }
-.emoji-1F1FA-1F1EC { background-position: -320px -240px; }
-.emoji-1F1FA-1F1F2 { background-position: -320px -260px; }
-.emoji-1F1FA-1F1F8 { background-position: -320px -280px; }
-.emoji-1F1FA-1F1FE { background-position: -320px -300px; }
-.emoji-1F1FA-1F1FF { background-position: 0 -320px; }
-.emoji-1F1FB-1F1E6 { background-position: -20px -320px; }
-.emoji-1F1FB-1F1E8 { background-position: -40px -320px; }
-.emoji-1F1FB-1F1EA { background-position: -60px -320px; }
-.emoji-1F1FB-1F1EC { background-position: -80px -320px; }
-.emoji-1F1FB-1F1EE { background-position: -100px -320px; }
-.emoji-1F1FB-1F1F3 { background-position: -120px -320px; }
-.emoji-1F1FB-1F1FA { background-position: -140px -320px; }
-.emoji-1F1FC-1F1EB { background-position: -160px -320px; }
-.emoji-1F1FC-1F1F8 { background-position: -180px -320px; }
-.emoji-1F1FD-1F1F0 { background-position: -200px -320px; }
-.emoji-1F1FE-1F1EA { background-position: -220px -320px; }
-.emoji-1F1FE-1F1F9 { background-position: -240px -320px; }
-.emoji-1F1FF-1F1E6 { background-position: -260px -320px; }
-.emoji-1F1FF-1F1F2 { background-position: -280px -320px; }
-.emoji-1F1FF-1F1FC { background-position: -300px -320px; }
-.emoji-1F201 { background-position: -320px -320px; }
-.emoji-1F202 { background-position: -340px 0; }
-.emoji-1F21A { background-position: -340px -20px; }
-.emoji-1F22F { background-position: -340px -40px; }
-.emoji-1F232 { background-position: -340px -60px; }
-.emoji-1F233 { background-position: -340px -80px; }
-.emoji-1F234 { background-position: -340px -100px; }
-.emoji-1F235 { background-position: -340px -120px; }
-.emoji-1F236 { background-position: -340px -140px; }
-.emoji-1F237 { background-position: -340px -160px; }
-.emoji-1F238 { background-position: -340px -180px; }
-.emoji-1F239 { background-position: -340px -200px; }
-.emoji-1F23A { background-position: -340px -220px; }
-.emoji-1F250 { background-position: -340px -240px; }
-.emoji-1F251 { background-position: -340px -260px; }
-.emoji-1F300 { background-position: -340px -280px; }
-.emoji-1F301 { background-position: -340px -300px; }
-.emoji-1F302 { background-position: -340px -320px; }
-.emoji-1F303 { background-position: 0 -340px; }
-.emoji-1F304 { background-position: -20px -340px; }
-.emoji-1F305 { background-position: -40px -340px; }
-.emoji-1F306 { background-position: -60px -340px; }
-.emoji-1F307 { background-position: -80px -340px; }
-.emoji-1F308 { background-position: -100px -340px; }
-.emoji-1F309 { background-position: -120px -340px; }
-.emoji-1F30A { background-position: -140px -340px; }
-.emoji-1F30B { background-position: -160px -340px; }
-.emoji-1F30C { background-position: -180px -340px; }
-.emoji-1F30D { background-position: -200px -340px; }
-.emoji-1F30E { background-position: -220px -340px; }
-.emoji-1F30F { background-position: -240px -340px; }
-.emoji-1F310 { background-position: -260px -340px; }
-.emoji-1F311 { background-position: -280px -340px; }
-.emoji-1F312 { background-position: -300px -340px; }
-.emoji-1F313 { background-position: -320px -340px; }
-.emoji-1F314 { background-position: -340px -340px; }
-.emoji-1F315 { background-position: -360px 0; }
-.emoji-1F316 { background-position: -360px -20px; }
-.emoji-1F317 { background-position: -360px -40px; }
-.emoji-1F318 { background-position: -360px -60px; }
-.emoji-1F319 { background-position: -360px -80px; }
-.emoji-1F31A { background-position: -360px -100px; }
-.emoji-1F31B { background-position: -360px -120px; }
-.emoji-1F31C { background-position: -360px -140px; }
-.emoji-1F31D { background-position: -360px -160px; }
-.emoji-1F31E { background-position: -360px -180px; }
-.emoji-1F31F { background-position: -360px -200px; }
-.emoji-1F320 { background-position: -360px -220px; }
-.emoji-1F321 { background-position: -360px -240px; }
-.emoji-1F324 { background-position: -360px -260px; }
-.emoji-1F325 { background-position: -360px -280px; }
-.emoji-1F326 { background-position: -360px -300px; }
-.emoji-1F327 { background-position: -360px -320px; }
-.emoji-1F328 { background-position: -360px -340px; }
-.emoji-1F329 { background-position: 0 -360px; }
-.emoji-1F32A { background-position: -20px -360px; }
-.emoji-1F32B { background-position: -40px -360px; }
-.emoji-1F32C { background-position: -60px -360px; }
-.emoji-1F32D { background-position: -80px -360px; }
-.emoji-1F32E { background-position: -100px -360px; }
-.emoji-1F32F { background-position: -120px -360px; }
-.emoji-1F330 { background-position: -140px -360px; }
-.emoji-1F331 { background-position: -160px -360px; }
-.emoji-1F332 { background-position: -180px -360px; }
-.emoji-1F333 { background-position: -200px -360px; }
-.emoji-1F334 { background-position: -220px -360px; }
-.emoji-1F335 { background-position: -240px -360px; }
-.emoji-1F336 { background-position: -260px -360px; }
-.emoji-1F337 { background-position: -280px -360px; }
-.emoji-1F338 { background-position: -300px -360px; }
-.emoji-1F339 { background-position: -320px -360px; }
-.emoji-1F33A { background-position: -340px -360px; }
-.emoji-1F33B { background-position: -360px -360px; }
-.emoji-1F33C { background-position: -380px 0; }
-.emoji-1F33D { background-position: -380px -20px; }
-.emoji-1F33E { background-position: -380px -40px; }
-.emoji-1F33F { background-position: -380px -60px; }
-.emoji-1F340 { background-position: -380px -80px; }
-.emoji-1F341 { background-position: -380px -100px; }
-.emoji-1F342 { background-position: -380px -120px; }
-.emoji-1F343 { background-position: -380px -140px; }
-.emoji-1F344 { background-position: -380px -160px; }
-.emoji-1F345 { background-position: -380px -180px; }
-.emoji-1F346 { background-position: -380px -200px; }
-.emoji-1F347 { background-position: -380px -220px; }
-.emoji-1F348 { background-position: -380px -240px; }
-.emoji-1F349 { background-position: -380px -260px; }
-.emoji-1F34A { background-position: -380px -280px; }
-.emoji-1F34B { background-position: -380px -300px; }
-.emoji-1F34C { background-position: -380px -320px; }
-.emoji-1F34D { background-position: -380px -340px; }
-.emoji-1F34E { background-position: -380px -360px; }
-.emoji-1F34F { background-position: 0 -380px; }
-.emoji-1F350 { background-position: -20px -380px; }
-.emoji-1F351 { background-position: -40px -380px; }
-.emoji-1F352 { background-position: -60px -380px; }
-.emoji-1F353 { background-position: -80px -380px; }
-.emoji-1F354 { background-position: -100px -380px; }
-.emoji-1F355 { background-position: -120px -380px; }
-.emoji-1F356 { background-position: -140px -380px; }
-.emoji-1F357 { background-position: -160px -380px; }
-.emoji-1F358 { background-position: -180px -380px; }
-.emoji-1F359 { background-position: -200px -380px; }
-.emoji-1F35A { background-position: -220px -380px; }
-.emoji-1F35B { background-position: -240px -380px; }
-.emoji-1F35C { background-position: -260px -380px; }
-.emoji-1F35D { background-position: -280px -380px; }
-.emoji-1F35E { background-position: -300px -380px; }
-.emoji-1F35F { background-position: -320px -380px; }
-.emoji-1F360 { background-position: -340px -380px; }
-.emoji-1F361 { background-position: -360px -380px; }
-.emoji-1F362 { background-position: -380px -380px; }
-.emoji-1F363 { background-position: -400px 0; }
-.emoji-1F364 { background-position: -400px -20px; }
-.emoji-1F365 { background-position: -400px -40px; }
-.emoji-1F366 { background-position: -400px -60px; }
-.emoji-1F367 { background-position: -400px -80px; }
-.emoji-1F368 { background-position: -400px -100px; }
-.emoji-1F369 { background-position: -400px -120px; }
-.emoji-1F36A { background-position: -400px -140px; }
-.emoji-1F36B { background-position: -400px -160px; }
-.emoji-1F36C { background-position: -400px -180px; }
-.emoji-1F36D { background-position: -400px -200px; }
-.emoji-1F36E { background-position: -400px -220px; }
-.emoji-1F36F { background-position: -400px -240px; }
-.emoji-1F370 { background-position: -400px -260px; }
-.emoji-1F371 { background-position: -400px -280px; }
-.emoji-1F372 { background-position: -400px -300px; }
-.emoji-1F373 { background-position: -400px -320px; }
-.emoji-1F374 { background-position: -400px -340px; }
-.emoji-1F375 { background-position: -400px -360px; }
-.emoji-1F376 { background-position: -400px -380px; }
-.emoji-1F377 { background-position: 0 -400px; }
-.emoji-1F378 { background-position: -20px -400px; }
-.emoji-1F379 { background-position: -40px -400px; }
-.emoji-1F37A { background-position: -60px -400px; }
-.emoji-1F37B { background-position: -80px -400px; }
-.emoji-1F37C { background-position: -100px -400px; }
-.emoji-1F37D { background-position: -120px -400px; }
-.emoji-1F37E { background-position: -140px -400px; }
-.emoji-1F37F { background-position: -160px -400px; }
-.emoji-1F380 { background-position: -180px -400px; }
-.emoji-1F381 { background-position: -200px -400px; }
-.emoji-1F382 { background-position: -220px -400px; }
-.emoji-1F383 { background-position: -240px -400px; }
-.emoji-1F384 { background-position: -260px -400px; }
-.emoji-1F385 { background-position: -280px -400px; }
-.emoji-1F385-1F3FB { background-position: -300px -400px; }
-.emoji-1F385-1F3FC { background-position: -320px -400px; }
-.emoji-1F385-1F3FD { background-position: -340px -400px; }
-.emoji-1F385-1F3FE { background-position: -360px -400px; }
-.emoji-1F385-1F3FF { background-position: -380px -400px; }
-.emoji-1F386 { background-position: -400px -400px; }
-.emoji-1F387 { background-position: -420px 0; }
-.emoji-1F388 { background-position: -420px -20px; }
-.emoji-1F389 { background-position: -420px -40px; }
-.emoji-1F38A { background-position: -420px -60px; }
-.emoji-1F38B { background-position: -420px -80px; }
-.emoji-1F38C { background-position: -420px -100px; }
-.emoji-1F38D { background-position: -420px -120px; }
-.emoji-1F38E { background-position: -420px -140px; }
-.emoji-1F38F { background-position: -420px -160px; }
-.emoji-1F390 { background-position: -420px -180px; }
-.emoji-1F391 { background-position: -420px -200px; }
-.emoji-1F392 { background-position: -420px -220px; }
-.emoji-1F393 { background-position: -420px -240px; }
-.emoji-1F396 { background-position: -420px -260px; }
-.emoji-1F397 { background-position: -420px -280px; }
-.emoji-1F399 { background-position: -420px -300px; }
-.emoji-1F39A { background-position: -420px -320px; }
-.emoji-1F39B { background-position: -420px -340px; }
-.emoji-1F39E { background-position: -420px -360px; }
-.emoji-1F39F { background-position: -420px -380px; }
-.emoji-1F3A0 { background-position: -420px -400px; }
-.emoji-1F3A1 { background-position: 0 -420px; }
-.emoji-1F3A2 { background-position: -20px -420px; }
-.emoji-1F3A3 { background-position: -40px -420px; }
-.emoji-1F3A4 { background-position: -60px -420px; }
-.emoji-1F3A5 { background-position: -80px -420px; }
-.emoji-1F3A6 { background-position: -100px -420px; }
-.emoji-1F3A7 { background-position: -120px -420px; }
-.emoji-1F3A8 { background-position: -140px -420px; }
-.emoji-1F3A9 { background-position: -160px -420px; }
-.emoji-1F3AA { background-position: -180px -420px; }
-.emoji-1F3AB { background-position: -200px -420px; }
-.emoji-1F3AC { background-position: -220px -420px; }
-.emoji-1F3AD { background-position: -240px -420px; }
-.emoji-1F3AE { background-position: -260px -420px; }
-.emoji-1F3AF { background-position: -280px -420px; }
-.emoji-1F3B0 { background-position: -300px -420px; }
-.emoji-1F3B1 { background-position: -320px -420px; }
-.emoji-1F3B2 { background-position: -340px -420px; }
-.emoji-1F3B3 { background-position: -360px -420px; }
-.emoji-1F3B4 { background-position: -380px -420px; }
-.emoji-1F3B5 { background-position: -400px -420px; }
-.emoji-1F3B6 { background-position: -420px -420px; }
-.emoji-1F3B7 { background-position: -440px 0; }
-.emoji-1F3B8 { background-position: -440px -20px; }
-.emoji-1F3B9 { background-position: -440px -40px; }
-.emoji-1F3BA { background-position: -440px -60px; }
-.emoji-1F3BB { background-position: -440px -80px; }
-.emoji-1F3BC { background-position: -440px -100px; }
-.emoji-1F3BD { background-position: -440px -120px; }
-.emoji-1F3BE { background-position: -440px -140px; }
-.emoji-1F3BF { background-position: -440px -160px; }
-.emoji-1F3C0 { background-position: -440px -180px; }
-.emoji-1F3C1 { background-position: -440px -200px; }
-.emoji-1F3C2 { background-position: -440px -220px; }
-.emoji-1F3C3 { background-position: -440px -240px; }
-.emoji-1F3C3-1F3FB { background-position: -440px -260px; }
-.emoji-1F3C3-1F3FC { background-position: -440px -280px; }
-.emoji-1F3C3-1F3FD { background-position: -440px -300px; }
-.emoji-1F3C3-1F3FE { background-position: -440px -320px; }
-.emoji-1F3C3-1F3FF { background-position: -440px -340px; }
-.emoji-1F3C4 { background-position: -440px -360px; }
-.emoji-1F3C4-1F3FB { background-position: -440px -380px; }
-.emoji-1F3C4-1F3FC { background-position: -440px -400px; }
-.emoji-1F3C4-1F3FD { background-position: -440px -420px; }
-.emoji-1F3C4-1F3FE { background-position: 0 -440px; }
-.emoji-1F3C4-1F3FF { background-position: -20px -440px; }
-.emoji-1F3C5 { background-position: -40px -440px; }
-.emoji-1F3C6 { background-position: -60px -440px; }
-.emoji-1F3C7 { background-position: -80px -440px; }
-.emoji-1F3C7-1F3FB { background-position: -100px -440px; }
-.emoji-1F3C7-1F3FC { background-position: -120px -440px; }
-.emoji-1F3C7-1F3FD { background-position: -140px -440px; }
-.emoji-1F3C7-1F3FE { background-position: -160px -440px; }
-.emoji-1F3C7-1F3FF { background-position: -180px -440px; }
-.emoji-1F3C8 { background-position: -200px -440px; }
-.emoji-1F3C9 { background-position: -220px -440px; }
-.emoji-1F3CA { background-position: -240px -440px; }
-.emoji-1F3CA-1F3FB { background-position: -260px -440px; }
-.emoji-1F3CA-1F3FC { background-position: -280px -440px; }
-.emoji-1F3CA-1F3FD { background-position: -300px -440px; }
-.emoji-1F3CA-1F3FE { background-position: -320px -440px; }
-.emoji-1F3CA-1F3FF { background-position: -340px -440px; }
-.emoji-1F3CB { background-position: -360px -440px; }
-.emoji-1F3CB-1F3FB { background-position: -380px -440px; }
-.emoji-1F3CB-1F3FC { background-position: -400px -440px; }
-.emoji-1F3CB-1F3FD { background-position: -420px -440px; }
-.emoji-1F3CB-1F3FE { background-position: -440px -440px; }
-.emoji-1F3CB-1F3FF { background-position: -460px 0; }
-.emoji-1F3CC { background-position: -460px -20px; }
-.emoji-1F3CD { background-position: -460px -40px; }
-.emoji-1F3CE { background-position: -460px -60px; }
-.emoji-1F3CF { background-position: -460px -80px; }
-.emoji-1F3D0 { background-position: -460px -100px; }
-.emoji-1F3D1 { background-position: -460px -120px; }
-.emoji-1F3D2 { background-position: -460px -140px; }
-.emoji-1F3D3 { background-position: -460px -160px; }
-.emoji-1F3D4 { background-position: -460px -180px; }
-.emoji-1F3D5 { background-position: -460px -200px; }
-.emoji-1F3D6 { background-position: -460px -220px; }
-.emoji-1F3D7 { background-position: -460px -240px; }
-.emoji-1F3D8 { background-position: -460px -260px; }
-.emoji-1F3D9 { background-position: -460px -280px; }
-.emoji-1F3DA { background-position: -460px -300px; }
-.emoji-1F3DB { background-position: -460px -320px; }
-.emoji-1F3DC { background-position: -460px -340px; }
-.emoji-1F3DD { background-position: -460px -360px; }
-.emoji-1F3DE { background-position: -460px -380px; }
-.emoji-1F3DF { background-position: -460px -400px; }
-.emoji-1F3E0 { background-position: -460px -420px; }
-.emoji-1F3E1 { background-position: -460px -440px; }
-.emoji-1F3E2 { background-position: 0 -460px; }
-.emoji-1F3E3 { background-position: -20px -460px; }
-.emoji-1F3E4 { background-position: -40px -460px; }
-.emoji-1F3E5 { background-position: -60px -460px; }
-.emoji-1F3E6 { background-position: -80px -460px; }
-.emoji-1F3E7 { background-position: -100px -460px; }
-.emoji-1F3E8 { background-position: -120px -460px; }
-.emoji-1F3E9 { background-position: -140px -460px; }
-.emoji-1F3EA { background-position: -160px -460px; }
-.emoji-1F3EB { background-position: -180px -460px; }
-.emoji-1F3EC { background-position: -200px -460px; }
-.emoji-1F3ED { background-position: -220px -460px; }
-.emoji-1F3EE { background-position: -240px -460px; }
-.emoji-1F3EF { background-position: -260px -460px; }
-.emoji-1F3F0 { background-position: -280px -460px; }
-.emoji-1F3F3 { background-position: -300px -460px; }
-.emoji-1F3F4 { background-position: -320px -460px; }
-.emoji-1F3F5 { background-position: -340px -460px; }
-.emoji-1F3F7 { background-position: -360px -460px; }
-.emoji-1F3F8 { background-position: -380px -460px; }
-.emoji-1F3F9 { background-position: -400px -460px; }
-.emoji-1F3FA { background-position: -420px -460px; }
-.emoji-1F3FB { background-position: -440px -460px; }
-.emoji-1F3FC { background-position: -460px -460px; }
-.emoji-1F3FD { background-position: -480px 0; }
-.emoji-1F3FE { background-position: -480px -20px; }
-.emoji-1F3FF { background-position: -480px -40px; }
-.emoji-1F400 { background-position: -480px -60px; }
-.emoji-1F401 { background-position: -480px -80px; }
-.emoji-1F402 { background-position: -480px -100px; }
-.emoji-1F403 { background-position: -480px -120px; }
-.emoji-1F404 { background-position: -480px -140px; }
-.emoji-1F405 { background-position: -480px -160px; }
-.emoji-1F406 { background-position: -480px -180px; }
-.emoji-1F407 { background-position: -480px -200px; }
-.emoji-1F408 { background-position: -480px -220px; }
-.emoji-1F409 { background-position: -480px -240px; }
-.emoji-1F40A { background-position: -480px -260px; }
-.emoji-1F40B { background-position: -480px -280px; }
-.emoji-1F40C { background-position: -480px -300px; }
-.emoji-1F40D { background-position: -480px -320px; }
-.emoji-1F40E { background-position: -480px -340px; }
-.emoji-1F40F { background-position: -480px -360px; }
-.emoji-1F410 { background-position: -480px -380px; }
-.emoji-1F411 { background-position: -480px -400px; }
-.emoji-1F412 { background-position: -480px -420px; }
-.emoji-1F413 { background-position: -480px -440px; }
-.emoji-1F414 { background-position: -480px -460px; }
-.emoji-1F415 { background-position: 0 -480px; }
-.emoji-1F416 { background-position: -20px -480px; }
-.emoji-1F417 { background-position: -40px -480px; }
-.emoji-1F418 { background-position: -60px -480px; }
-.emoji-1F419 { background-position: -80px -480px; }
-.emoji-1F41A { background-position: -100px -480px; }
-.emoji-1F41B { background-position: -120px -480px; }
-.emoji-1F41C { background-position: -140px -480px; }
-.emoji-1F41D { background-position: -160px -480px; }
-.emoji-1F41E { background-position: -180px -480px; }
-.emoji-1F41F { background-position: -200px -480px; }
-.emoji-1F420 { background-position: -220px -480px; }
-.emoji-1F421 { background-position: -240px -480px; }
-.emoji-1F422 { background-position: -260px -480px; }
-.emoji-1F423 { background-position: -280px -480px; }
-.emoji-1F424 { background-position: -300px -480px; }
-.emoji-1F425 { background-position: -320px -480px; }
-.emoji-1F426 { background-position: -340px -480px; }
-.emoji-1F427 { background-position: -360px -480px; }
-.emoji-1F428 { background-position: -380px -480px; }
-.emoji-1F429 { background-position: -400px -480px; }
-.emoji-1F42A { background-position: -420px -480px; }
-.emoji-1F42B { background-position: -440px -480px; }
-.emoji-1F42C { background-position: -460px -480px; }
-.emoji-1F42D { background-position: -480px -480px; }
-.emoji-1F42E { background-position: -500px 0; }
-.emoji-1F42F { background-position: -500px -20px; }
-.emoji-1F430 { background-position: -500px -40px; }
-.emoji-1F431 { background-position: -500px -60px; }
-.emoji-1F432 { background-position: -500px -80px; }
-.emoji-1F433 { background-position: -500px -100px; }
-.emoji-1F434 { background-position: -500px -120px; }
-.emoji-1F435 { background-position: -500px -140px; }
-.emoji-1F436 { background-position: -500px -160px; }
-.emoji-1F437 { background-position: -500px -180px; }
-.emoji-1F438 { background-position: -500px -200px; }
-.emoji-1F439 { background-position: -500px -220px; }
-.emoji-1F43A { background-position: -500px -240px; }
-.emoji-1F43B { background-position: -500px -260px; }
-.emoji-1F43C { background-position: -500px -280px; }
-.emoji-1F43D { background-position: -500px -300px; }
-.emoji-1F43E { background-position: -500px -320px; }
-.emoji-1F43F { background-position: -500px -340px; }
-.emoji-1F440 { background-position: -500px -360px; }
-.emoji-1F441 { background-position: -500px -380px; }
-.emoji-1F441-1F5E8 { background-position: -500px -400px; }
-.emoji-1F442 { background-position: -500px -420px; }
-.emoji-1F442-1F3FB { background-position: -500px -440px; }
-.emoji-1F442-1F3FC { background-position: -500px -460px; }
-.emoji-1F442-1F3FD { background-position: -500px -480px; }
-.emoji-1F442-1F3FE { background-position: 0 -500px; }
-.emoji-1F442-1F3FF { background-position: -20px -500px; }
-.emoji-1F443 { background-position: -40px -500px; }
-.emoji-1F443-1F3FB { background-position: -60px -500px; }
-.emoji-1F443-1F3FC { background-position: -80px -500px; }
-.emoji-1F443-1F3FD { background-position: -100px -500px; }
-.emoji-1F443-1F3FE { background-position: -120px -500px; }
-.emoji-1F443-1F3FF { background-position: -140px -500px; }
-.emoji-1F444 { background-position: -160px -500px; }
-.emoji-1F445 { background-position: -180px -500px; }
-.emoji-1F446 { background-position: -200px -500px; }
-.emoji-1F446-1F3FB { background-position: -220px -500px; }
-.emoji-1F446-1F3FC { background-position: -240px -500px; }
-.emoji-1F446-1F3FD { background-position: -260px -500px; }
-.emoji-1F446-1F3FE { background-position: -280px -500px; }
-.emoji-1F446-1F3FF { background-position: -300px -500px; }
-.emoji-1F447 { background-position: -320px -500px; }
-.emoji-1F447-1F3FB { background-position: -340px -500px; }
-.emoji-1F447-1F3FC { background-position: -360px -500px; }
-.emoji-1F447-1F3FD { background-position: -380px -500px; }
-.emoji-1F447-1F3FE { background-position: -400px -500px; }
-.emoji-1F447-1F3FF { background-position: -420px -500px; }
-.emoji-1F448 { background-position: -440px -500px; }
-.emoji-1F448-1F3FB { background-position: -460px -500px; }
-.emoji-1F448-1F3FC { background-position: -480px -500px; }
-.emoji-1F448-1F3FD { background-position: -500px -500px; }
-.emoji-1F448-1F3FE { background-position: -520px 0; }
-.emoji-1F448-1F3FF { background-position: -520px -20px; }
-.emoji-1F449 { background-position: -520px -40px; }
-.emoji-1F449-1F3FB { background-position: -520px -60px; }
-.emoji-1F449-1F3FC { background-position: -520px -80px; }
-.emoji-1F449-1F3FD { background-position: -520px -100px; }
-.emoji-1F449-1F3FE { background-position: -520px -120px; }
-.emoji-1F449-1F3FF { background-position: -520px -140px; }
-.emoji-1F44A { background-position: -520px -160px; }
-.emoji-1F44A-1F3FB { background-position: -520px -180px; }
-.emoji-1F44A-1F3FC { background-position: -520px -200px; }
-.emoji-1F44A-1F3FD { background-position: -520px -220px; }
-.emoji-1F44A-1F3FE { background-position: -520px -240px; }
-.emoji-1F44A-1F3FF { background-position: -520px -260px; }
-.emoji-1F44B { background-position: -520px -280px; }
-.emoji-1F44B-1F3FB { background-position: -520px -300px; }
-.emoji-1F44B-1F3FC { background-position: -520px -320px; }
-.emoji-1F44B-1F3FD { background-position: -520px -340px; }
-.emoji-1F44B-1F3FE { background-position: -520px -360px; }
-.emoji-1F44B-1F3FF { background-position: -520px -380px; }
-.emoji-1F44C { background-position: -520px -400px; }
-.emoji-1F44C-1F3FB { background-position: -520px -420px; }
-.emoji-1F44C-1F3FC { background-position: -520px -440px; }
-.emoji-1F44C-1F3FD { background-position: -520px -460px; }
-.emoji-1F44C-1F3FE { background-position: -520px -480px; }
-.emoji-1F44C-1F3FF { background-position: -520px -500px; }
-.emoji-1F44D { background-position: 0 -520px; }
-.emoji-1F44D-1F3FB { background-position: -20px -520px; }
-.emoji-1F44D-1F3FC { background-position: -40px -520px; }
-.emoji-1F44D-1F3FD { background-position: -60px -520px; }
-.emoji-1F44D-1F3FE { background-position: -80px -520px; }
-.emoji-1F44D-1F3FF { background-position: -100px -520px; }
-.emoji-1F44E { background-position: -120px -520px; }
-.emoji-1F44E-1F3FB { background-position: -140px -520px; }
-.emoji-1F44E-1F3FC { background-position: -160px -520px; }
-.emoji-1F44E-1F3FD { background-position: -180px -520px; }
-.emoji-1F44E-1F3FE { background-position: -200px -520px; }
-.emoji-1F44E-1F3FF { background-position: -220px -520px; }
-.emoji-1F44F { background-position: -240px -520px; }
-.emoji-1F44F-1F3FB { background-position: -260px -520px; }
-.emoji-1F44F-1F3FC { background-position: -280px -520px; }
-.emoji-1F44F-1F3FD { background-position: -300px -520px; }
-.emoji-1F44F-1F3FE { background-position: -320px -520px; }
-.emoji-1F44F-1F3FF { background-position: -340px -520px; }
-.emoji-1F450 { background-position: -360px -520px; }
-.emoji-1F450-1F3FB { background-position: -380px -520px; }
-.emoji-1F450-1F3FC { background-position: -400px -520px; }
-.emoji-1F450-1F3FD { background-position: -420px -520px; }
-.emoji-1F450-1F3FE { background-position: -440px -520px; }
-.emoji-1F450-1F3FF { background-position: -460px -520px; }
-.emoji-1F451 { background-position: -480px -520px; }
-.emoji-1F452 { background-position: -500px -520px; }
-.emoji-1F453 { background-position: -520px -520px; }
-.emoji-1F454 { background-position: -540px 0; }
-.emoji-1F455 { background-position: -540px -20px; }
-.emoji-1F456 { background-position: -540px -40px; }
-.emoji-1F457 { background-position: -540px -60px; }
-.emoji-1F458 { background-position: -540px -80px; }
-.emoji-1F459 { background-position: -540px -100px; }
-.emoji-1F45A { background-position: -540px -120px; }
-.emoji-1F45B { background-position: -540px -140px; }
-.emoji-1F45C { background-position: -540px -160px; }
-.emoji-1F45D { background-position: -540px -180px; }
-.emoji-1F45E { background-position: -540px -200px; }
-.emoji-1F45F { background-position: -540px -220px; }
-.emoji-1F460 { background-position: -540px -240px; }
-.emoji-1F461 { background-position: -540px -260px; }
-.emoji-1F462 { background-position: -540px -280px; }
-.emoji-1F463 { background-position: -540px -300px; }
-.emoji-1F464 { background-position: -540px -320px; }
-.emoji-1F465 { background-position: -540px -340px; }
-.emoji-1F466 { background-position: -540px -360px; }
-.emoji-1F466-1F3FB { background-position: -540px -380px; }
-.emoji-1F466-1F3FC { background-position: -540px -400px; }
-.emoji-1F466-1F3FD { background-position: -540px -420px; }
-.emoji-1F466-1F3FE { background-position: -540px -440px; }
-.emoji-1F466-1F3FF { background-position: -540px -460px; }
-.emoji-1F467 { background-position: -540px -480px; }
-.emoji-1F467-1F3FB { background-position: -540px -500px; }
-.emoji-1F467-1F3FC { background-position: -540px -520px; }
-.emoji-1F467-1F3FD { background-position: 0 -540px; }
-.emoji-1F467-1F3FE { background-position: -20px -540px; }
-.emoji-1F467-1F3FF { background-position: -40px -540px; }
-.emoji-1F468 { background-position: -60px -540px; }
-.emoji-1F468-1F3FB { background-position: -80px -540px; }
-.emoji-1F468-1F3FC { background-position: -100px -540px; }
-.emoji-1F468-1F3FD { background-position: -120px -540px; }
-.emoji-1F468-1F3FE { background-position: -140px -540px; }
-.emoji-1F468-1F3FF { background-position: -160px -540px; }
-.emoji-1F468-1F468-1F466 { background-position: -180px -540px; }
-.emoji-1F468-1F468-1F466-1F466 { background-position: -200px -540px; }
-.emoji-1F468-1F468-1F467 { background-position: -220px -540px; }
-.emoji-1F468-1F468-1F467-1F466 { background-position: -240px -540px; }
-.emoji-1F468-1F468-1F467-1F467 { background-position: -260px -540px; }
-.emoji-1F468-1F469-1F466-1F466 { background-position: -280px -540px; }
-.emoji-1F468-1F469-1F467 { background-position: -300px -540px; }
-.emoji-1F468-1F469-1F467-1F466 { background-position: -320px -540px; }
-.emoji-1F468-1F469-1F467-1F467 { background-position: -340px -540px; }
-.emoji-1F468-2764-1F468 { background-position: -360px -540px; }
-.emoji-1F468-2764-1F48B-1F468 { background-position: -380px -540px; }
-.emoji-1F469 { background-position: -400px -540px; }
-.emoji-1F469-1F3FB { background-position: -420px -540px; }
-.emoji-1F469-1F3FC { background-position: -440px -540px; }
-.emoji-1F469-1F3FD { background-position: -460px -540px; }
-.emoji-1F469-1F3FE { background-position: -480px -540px; }
-.emoji-1F469-1F3FF { background-position: -500px -540px; }
-.emoji-1F469-1F469-1F466 { background-position: -520px -540px; }
-.emoji-1F469-1F469-1F466-1F466 { background-position: -540px -540px; }
-.emoji-1F469-1F469-1F467 { background-position: -560px 0; }
-.emoji-1F469-1F469-1F467-1F466 { background-position: -560px -20px; }
-.emoji-1F469-1F469-1F467-1F467 { background-position: -560px -40px; }
-.emoji-1F469-2764-1F469 { background-position: -560px -60px; }
-.emoji-1F469-2764-1F48B-1F469 { background-position: -560px -80px; }
-.emoji-1F46A { background-position: -560px -100px; }
-.emoji-1F46B { background-position: -560px -120px; }
-.emoji-1F46C { background-position: -560px -140px; }
-.emoji-1F46D { background-position: -560px -160px; }
-.emoji-1F46E { background-position: -560px -180px; }
-.emoji-1F46E-1F3FB { background-position: -560px -200px; }
-.emoji-1F46E-1F3FC { background-position: -560px -220px; }
-.emoji-1F46E-1F3FD { background-position: -560px -240px; }
-.emoji-1F46E-1F3FE { background-position: -560px -260px; }
-.emoji-1F46E-1F3FF { background-position: -560px -280px; }
-.emoji-1F46F { background-position: -560px -300px; }
-.emoji-1F470 { background-position: -560px -320px; }
-.emoji-1F470-1F3FB { background-position: -560px -340px; }
-.emoji-1F470-1F3FC { background-position: -560px -360px; }
-.emoji-1F470-1F3FD { background-position: -560px -380px; }
-.emoji-1F470-1F3FE { background-position: -560px -400px; }
-.emoji-1F470-1F3FF { background-position: -560px -420px; }
-.emoji-1F471 { background-position: -560px -440px; }
-.emoji-1F471-1F3FB { background-position: -560px -460px; }
-.emoji-1F471-1F3FC { background-position: -560px -480px; }
-.emoji-1F471-1F3FD { background-position: -560px -500px; }
-.emoji-1F471-1F3FE { background-position: -560px -520px; }
-.emoji-1F471-1F3FF { background-position: -560px -540px; }
-.emoji-1F472 { background-position: 0 -560px; }
-.emoji-1F472-1F3FB { background-position: -20px -560px; }
-.emoji-1F472-1F3FC { background-position: -40px -560px; }
-.emoji-1F472-1F3FD { background-position: -60px -560px; }
-.emoji-1F472-1F3FE { background-position: -80px -560px; }
-.emoji-1F472-1F3FF { background-position: -100px -560px; }
-.emoji-1F473 { background-position: -120px -560px; }
-.emoji-1F473-1F3FB { background-position: -140px -560px; }
-.emoji-1F473-1F3FC { background-position: -160px -560px; }
-.emoji-1F473-1F3FD { background-position: -180px -560px; }
-.emoji-1F473-1F3FE { background-position: -200px -560px; }
-.emoji-1F473-1F3FF { background-position: -220px -560px; }
-.emoji-1F474 { background-position: -240px -560px; }
-.emoji-1F474-1F3FB { background-position: -260px -560px; }
-.emoji-1F474-1F3FC { background-position: -280px -560px; }
-.emoji-1F474-1F3FD { background-position: -300px -560px; }
-.emoji-1F474-1F3FE { background-position: -320px -560px; }
-.emoji-1F474-1F3FF { background-position: -340px -560px; }
-.emoji-1F475 { background-position: -360px -560px; }
-.emoji-1F475-1F3FB { background-position: -380px -560px; }
-.emoji-1F475-1F3FC { background-position: -400px -560px; }
-.emoji-1F475-1F3FD { background-position: -420px -560px; }
-.emoji-1F475-1F3FE { background-position: -440px -560px; }
-.emoji-1F475-1F3FF { background-position: -460px -560px; }
-.emoji-1F476 { background-position: -480px -560px; }
-.emoji-1F476-1F3FB { background-position: -500px -560px; }
-.emoji-1F476-1F3FC { background-position: -520px -560px; }
-.emoji-1F476-1F3FD { background-position: -540px -560px; }
-.emoji-1F476-1F3FE { background-position: -560px -560px; }
-.emoji-1F476-1F3FF { background-position: -580px 0; }
-.emoji-1F477 { background-position: -580px -20px; }
-.emoji-1F477-1F3FB { background-position: -580px -40px; }
-.emoji-1F477-1F3FC { background-position: -580px -60px; }
-.emoji-1F477-1F3FD { background-position: -580px -80px; }
-.emoji-1F477-1F3FE { background-position: -580px -100px; }
-.emoji-1F477-1F3FF { background-position: -580px -120px; }
-.emoji-1F478 { background-position: -580px -140px; }
-.emoji-1F478-1F3FB { background-position: -580px -160px; }
-.emoji-1F478-1F3FC { background-position: -580px -180px; }
-.emoji-1F478-1F3FD { background-position: -580px -200px; }
-.emoji-1F478-1F3FE { background-position: -580px -220px; }
-.emoji-1F478-1F3FF { background-position: -580px -240px; }
-.emoji-1F479 { background-position: -580px -260px; }
-.emoji-1F47A { background-position: -580px -280px; }
-.emoji-1F47B { background-position: -580px -300px; }
-.emoji-1F47C { background-position: -580px -320px; }
-.emoji-1F47C-1F3FB { background-position: -580px -340px; }
-.emoji-1F47C-1F3FC { background-position: -580px -360px; }
-.emoji-1F47C-1F3FD { background-position: -580px -380px; }
-.emoji-1F47C-1F3FE { background-position: -580px -400px; }
-.emoji-1F47C-1F3FF { background-position: -580px -420px; }
-.emoji-1F47D { background-position: -580px -440px; }
-.emoji-1F47E { background-position: -580px -460px; }
-.emoji-1F47F { background-position: -580px -480px; }
-.emoji-1F480 { background-position: -580px -500px; }
-.emoji-1F481 { background-position: -580px -520px; }
-.emoji-1F481-1F3FB { background-position: -580px -540px; }
-.emoji-1F481-1F3FC { background-position: -580px -560px; }
-.emoji-1F481-1F3FD { background-position: 0 -580px; }
-.emoji-1F481-1F3FE { background-position: -20px -580px; }
-.emoji-1F481-1F3FF { background-position: -40px -580px; }
-.emoji-1F482 { background-position: -60px -580px; }
-.emoji-1F482-1F3FB { background-position: -80px -580px; }
-.emoji-1F482-1F3FC { background-position: -100px -580px; }
-.emoji-1F482-1F3FD { background-position: -120px -580px; }
-.emoji-1F482-1F3FE { background-position: -140px -580px; }
-.emoji-1F482-1F3FF { background-position: -160px -580px; }
-.emoji-1F483 { background-position: -180px -580px; }
-.emoji-1F483-1F3FB { background-position: -200px -580px; }
-.emoji-1F483-1F3FC { background-position: -220px -580px; }
-.emoji-1F483-1F3FD { background-position: -240px -580px; }
-.emoji-1F483-1F3FE { background-position: -260px -580px; }
-.emoji-1F483-1F3FF { background-position: -280px -580px; }
-.emoji-1F484 { background-position: -300px -580px; }
-.emoji-1F485 { background-position: -320px -580px; }
-.emoji-1F485-1F3FB { background-position: -340px -580px; }
-.emoji-1F485-1F3FC { background-position: -360px -580px; }
-.emoji-1F485-1F3FD { background-position: -380px -580px; }
-.emoji-1F485-1F3FE { background-position: -400px -580px; }
-.emoji-1F485-1F3FF { background-position: -420px -580px; }
-.emoji-1F486 { background-position: -440px -580px; }
-.emoji-1F486-1F3FB { background-position: -460px -580px; }
-.emoji-1F486-1F3FC { background-position: -480px -580px; }
-.emoji-1F486-1F3FD { background-position: -500px -580px; }
-.emoji-1F486-1F3FE { background-position: -520px -580px; }
-.emoji-1F486-1F3FF { background-position: -540px -580px; }
-.emoji-1F487 { background-position: -560px -580px; }
-.emoji-1F487-1F3FB { background-position: -580px -580px; }
-.emoji-1F487-1F3FC { background-position: -600px 0; }
-.emoji-1F487-1F3FD { background-position: -600px -20px; }
-.emoji-1F487-1F3FE { background-position: -600px -40px; }
-.emoji-1F487-1F3FF { background-position: -600px -60px; }
-.emoji-1F488 { background-position: -600px -80px; }
-.emoji-1F489 { background-position: -600px -100px; }
-.emoji-1F48A { background-position: -600px -120px; }
-.emoji-1F48B { background-position: -600px -140px; }
-.emoji-1F48C { background-position: -600px -160px; }
-.emoji-1F48D { background-position: -600px -180px; }
-.emoji-1F48E { background-position: -600px -200px; }
-.emoji-1F48F { background-position: -600px -220px; }
-.emoji-1F490 { background-position: -600px -240px; }
-.emoji-1F491 { background-position: -600px -260px; }
-.emoji-1F492 { background-position: -600px -280px; }
-.emoji-1F493 { background-position: -600px -300px; }
-.emoji-1F494 { background-position: -600px -320px; }
-.emoji-1F495 { background-position: -600px -340px; }
-.emoji-1F496 { background-position: -600px -360px; }
-.emoji-1F497 { background-position: -600px -380px; }
-.emoji-1F498 { background-position: -600px -400px; }
-.emoji-1F499 { background-position: -600px -420px; }
-.emoji-1F49A { background-position: -600px -440px; }
-.emoji-1F49B { background-position: -600px -460px; }
-.emoji-1F49C { background-position: -600px -480px; }
-.emoji-1F49D { background-position: -600px -500px; }
-.emoji-1F49E { background-position: -600px -520px; }
-.emoji-1F49F { background-position: -600px -540px; }
-.emoji-1F4A0 { background-position: -600px -560px; }
-.emoji-1F4A1 { background-position: -600px -580px; }
-.emoji-1F4A2 { background-position: 0 -600px; }
-.emoji-1F4A3 { background-position: -20px -600px; }
-.emoji-1F4A4 { background-position: -40px -600px; }
-.emoji-1F4A5 { background-position: -60px -600px; }
-.emoji-1F4A6 { background-position: -80px -600px; }
-.emoji-1F4A7 { background-position: -100px -600px; }
-.emoji-1F4A8 { background-position: -120px -600px; }
-.emoji-1F4A9 { background-position: -140px -600px; }
-.emoji-1F4AA { background-position: -160px -600px; }
-.emoji-1F4AA-1F3FB { background-position: -180px -600px; }
-.emoji-1F4AA-1F3FC { background-position: -200px -600px; }
-.emoji-1F4AA-1F3FD { background-position: -220px -600px; }
-.emoji-1F4AA-1F3FE { background-position: -240px -600px; }
-.emoji-1F4AA-1F3FF { background-position: -260px -600px; }
-.emoji-1F4AB { background-position: -280px -600px; }
-.emoji-1F4AC { background-position: -300px -600px; }
-.emoji-1F4AD { background-position: -320px -600px; }
-.emoji-1F4AE { background-position: -340px -600px; }
-.emoji-1F4AF { background-position: -360px -600px; }
-.emoji-1F4B0 { background-position: -380px -600px; }
-.emoji-1F4B1 { background-position: -400px -600px; }
-.emoji-1F4B2 { background-position: -420px -600px; }
-.emoji-1F4B3 { background-position: -440px -600px; }
-.emoji-1F4B4 { background-position: -460px -600px; }
-.emoji-1F4B5 { background-position: -480px -600px; }
-.emoji-1F4B6 { background-position: -500px -600px; }
-.emoji-1F4B7 { background-position: -520px -600px; }
-.emoji-1F4B8 { background-position: -540px -600px; }
-.emoji-1F4B9 { background-position: -560px -600px; }
-.emoji-1F4BA { background-position: -580px -600px; }
-.emoji-1F4BB { background-position: -600px -600px; }
-.emoji-1F4BC { background-position: -620px 0; }
-.emoji-1F4BD { background-position: -620px -20px; }
-.emoji-1F4BE { background-position: -620px -40px; }
-.emoji-1F4BF { background-position: -620px -60px; }
-.emoji-1F4C0 { background-position: -620px -80px; }
-.emoji-1F4C1 { background-position: -620px -100px; }
-.emoji-1F4C2 { background-position: -620px -120px; }
-.emoji-1F4C3 { background-position: -620px -140px; }
-.emoji-1F4C4 { background-position: -620px -160px; }
-.emoji-1F4C5 { background-position: -620px -180px; }
-.emoji-1F4C6 { background-position: -620px -200px; }
-.emoji-1F4C7 { background-position: -620px -220px; }
-.emoji-1F4C8 { background-position: -620px -240px; }
-.emoji-1F4C9 { background-position: -620px -260px; }
-.emoji-1F4CA { background-position: -620px -280px; }
-.emoji-1F4CB { background-position: -620px -300px; }
-.emoji-1F4CC { background-position: -620px -320px; }
-.emoji-1F4CD { background-position: -620px -340px; }
-.emoji-1F4CE { background-position: -620px -360px; }
-.emoji-1F4CF { background-position: -620px -380px; }
-.emoji-1F4D0 { background-position: -620px -400px; }
-.emoji-1F4D1 { background-position: -620px -420px; }
-.emoji-1F4D2 { background-position: -620px -440px; }
-.emoji-1F4D3 { background-position: -620px -460px; }
-.emoji-1F4D4 { background-position: -620px -480px; }
-.emoji-1F4D5 { background-position: -620px -500px; }
-.emoji-1F4D6 { background-position: -620px -520px; }
-.emoji-1F4D7 { background-position: -620px -540px; }
-.emoji-1F4D8 { background-position: -620px -560px; }
-.emoji-1F4D9 { background-position: -620px -580px; }
-.emoji-1F4DA { background-position: -620px -600px; }
-.emoji-1F4DB { background-position: 0 -620px; }
-.emoji-1F4DC { background-position: -20px -620px; }
-.emoji-1F4DD { background-position: -40px -620px; }
-.emoji-1F4DE { background-position: -60px -620px; }
-.emoji-1F4DF { background-position: -80px -620px; }
-.emoji-1F4E0 { background-position: -100px -620px; }
-.emoji-1F4E1 { background-position: -120px -620px; }
-.emoji-1F4E2 { background-position: -140px -620px; }
-.emoji-1F4E3 { background-position: -160px -620px; }
-.emoji-1F4E4 { background-position: -180px -620px; }
-.emoji-1F4E5 { background-position: -200px -620px; }
-.emoji-1F4E6 { background-position: -220px -620px; }
-.emoji-1F4E7 { background-position: -240px -620px; }
-.emoji-1F4E8 { background-position: -260px -620px; }
-.emoji-1F4E9 { background-position: -280px -620px; }
-.emoji-1F4EA { background-position: -300px -620px; }
-.emoji-1F4EB { background-position: -320px -620px; }
-.emoji-1F4EC { background-position: -340px -620px; }
-.emoji-1F4ED { background-position: -360px -620px; }
-.emoji-1F4EE { background-position: -380px -620px; }
-.emoji-1F4EF { background-position: -400px -620px; }
-.emoji-1F4F0 { background-position: -420px -620px; }
-.emoji-1F4F1 { background-position: -440px -620px; }
-.emoji-1F4F2 { background-position: -460px -620px; }
-.emoji-1F4F3 { background-position: -480px -620px; }
-.emoji-1F4F4 { background-position: -500px -620px; }
-.emoji-1F4F5 { background-position: -520px -620px; }
-.emoji-1F4F6 { background-position: -540px -620px; }
-.emoji-1F4F7 { background-position: -560px -620px; }
-.emoji-1F4F8 { background-position: -580px -620px; }
-.emoji-1F4F9 { background-position: -600px -620px; }
-.emoji-1F4FA { background-position: -620px -620px; }
-.emoji-1F4FB { background-position: -640px 0; }
-.emoji-1F4FC { background-position: -640px -20px; }
-.emoji-1F4FD { background-position: -640px -40px; }
-.emoji-1F4FF { background-position: -640px -60px; }
-.emoji-1F500 { background-position: -640px -80px; }
-.emoji-1F501 { background-position: -640px -100px; }
-.emoji-1F502 { background-position: -640px -120px; }
-.emoji-1F503 { background-position: -640px -140px; }
-.emoji-1F504 { background-position: -640px -160px; }
-.emoji-1F505 { background-position: -640px -180px; }
-.emoji-1F506 { background-position: -640px -200px; }
-.emoji-1F507 { background-position: -640px -220px; }
-.emoji-1F508 { background-position: -640px -240px; }
-.emoji-1F509 { background-position: -640px -260px; }
-.emoji-1F50A { background-position: -640px -280px; }
-.emoji-1F50B { background-position: -640px -300px; }
-.emoji-1F50C { background-position: -640px -320px; }
-.emoji-1F50D { background-position: -640px -340px; }
-.emoji-1F50E { background-position: -640px -360px; }
-.emoji-1F50F { background-position: -640px -380px; }
-.emoji-1F510 { background-position: -640px -400px; }
-.emoji-1F511 { background-position: -640px -420px; }
-.emoji-1F512 { background-position: -640px -440px; }
-.emoji-1F513 { background-position: -640px -460px; }
-.emoji-1F514 { background-position: -640px -480px; }
-.emoji-1F515 { background-position: -640px -500px; }
-.emoji-1F516 { background-position: -640px -520px; }
-.emoji-1F517 { background-position: -640px -540px; }
-.emoji-1F518 { background-position: -640px -560px; }
-.emoji-1F519 { background-position: -640px -580px; }
-.emoji-1F51A { background-position: -640px -600px; }
-.emoji-1F51B { background-position: -640px -620px; }
-.emoji-1F51C { background-position: 0 -640px; }
-.emoji-1F51D { background-position: -20px -640px; }
-.emoji-1F51E { background-position: -40px -640px; }
-.emoji-1F51F { background-position: -60px -640px; }
-.emoji-1F520 { background-position: -80px -640px; }
-.emoji-1F521 { background-position: -100px -640px; }
-.emoji-1F522 { background-position: -120px -640px; }
-.emoji-1F523 { background-position: -140px -640px; }
-.emoji-1F524 { background-position: -160px -640px; }
-.emoji-1F525 { background-position: -180px -640px; }
-.emoji-1F526 { background-position: -200px -640px; }
-.emoji-1F527 { background-position: -220px -640px; }
-.emoji-1F528 { background-position: -240px -640px; }
-.emoji-1F529 { background-position: -260px -640px; }
-.emoji-1F52A { background-position: -280px -640px; }
-.emoji-1F52B { background-position: -300px -640px; }
-.emoji-1F52C { background-position: -320px -640px; }
-.emoji-1F52D { background-position: -340px -640px; }
-.emoji-1F52E { background-position: -360px -640px; }
-.emoji-1F52F { background-position: -380px -640px; }
-.emoji-1F530 { background-position: -400px -640px; }
-.emoji-1F531 { background-position: -420px -640px; }
-.emoji-1F532 { background-position: -440px -640px; }
-.emoji-1F533 { background-position: -460px -640px; }
-.emoji-1F534 { background-position: -480px -640px; }
-.emoji-1F535 { background-position: -500px -640px; }
-.emoji-1F536 { background-position: -520px -640px; }
-.emoji-1F537 { background-position: -540px -640px; }
-.emoji-1F538 { background-position: -560px -640px; }
-.emoji-1F539 { background-position: -580px -640px; }
-.emoji-1F53A { background-position: -600px -640px; }
-.emoji-1F53B { background-position: -620px -640px; }
-.emoji-1F53C { background-position: -640px -640px; }
-.emoji-1F53D { background-position: -660px 0; }
-.emoji-1F549 { background-position: -660px -20px; }
-.emoji-1F54A { background-position: -660px -40px; }
-.emoji-1F54B { background-position: -660px -60px; }
-.emoji-1F54C { background-position: -660px -80px; }
-.emoji-1F54D { background-position: -660px -100px; }
-.emoji-1F54E { background-position: -660px -120px; }
-.emoji-1F550 { background-position: -660px -140px; }
-.emoji-1F551 { background-position: -660px -160px; }
-.emoji-1F552 { background-position: -660px -180px; }
-.emoji-1F553 { background-position: -660px -200px; }
-.emoji-1F554 { background-position: -660px -220px; }
-.emoji-1F555 { background-position: -660px -240px; }
-.emoji-1F556 { background-position: -660px -260px; }
-.emoji-1F557 { background-position: -660px -280px; }
-.emoji-1F558 { background-position: -660px -300px; }
-.emoji-1F559 { background-position: -660px -320px; }
-.emoji-1F55A { background-position: -660px -340px; }
-.emoji-1F55B { background-position: -660px -360px; }
-.emoji-1F55C { background-position: -660px -380px; }
-.emoji-1F55D { background-position: -660px -400px; }
-.emoji-1F55E { background-position: -660px -420px; }
-.emoji-1F55F { background-position: -660px -440px; }
-.emoji-1F560 { background-position: -660px -460px; }
-.emoji-1F561 { background-position: -660px -480px; }
-.emoji-1F562 { background-position: -660px -500px; }
-.emoji-1F563 { background-position: -660px -520px; }
-.emoji-1F564 { background-position: -660px -540px; }
-.emoji-1F565 { background-position: -660px -560px; }
-.emoji-1F566 { background-position: -660px -580px; }
-.emoji-1F567 { background-position: -660px -600px; }
-.emoji-1F56F { background-position: -660px -620px; }
-.emoji-1F570 { background-position: -660px -640px; }
-.emoji-1F573 { background-position: 0 -660px; }
-.emoji-1F574 { background-position: -20px -660px; }
-.emoji-1F575 { background-position: -40px -660px; }
-.emoji-1F575-1F3FB { background-position: -60px -660px; }
-.emoji-1F575-1F3FC { background-position: -80px -660px; }
-.emoji-1F575-1F3FD { background-position: -100px -660px; }
-.emoji-1F575-1F3FE { background-position: -120px -660px; }
-.emoji-1F575-1F3FF { background-position: -140px -660px; }
-.emoji-1F576 { background-position: -160px -660px; }
-.emoji-1F577 { background-position: -180px -660px; }
-.emoji-1F578 { background-position: -200px -660px; }
-.emoji-1F579 { background-position: -220px -660px; }
-.emoji-1F57A { background-position: -240px -660px; }
-.emoji-1F57A-1F3FB { background-position: -260px -660px; }
-.emoji-1F57A-1F3FC { background-position: -280px -660px; }
-.emoji-1F57A-1F3FD { background-position: -300px -660px; }
-.emoji-1F57A-1F3FE { background-position: -320px -660px; }
-.emoji-1F57A-1F3FF { background-position: -340px -660px; }
-.emoji-1F587 { background-position: -360px -660px; }
-.emoji-1F58A { background-position: -380px -660px; }
-.emoji-1F58B { background-position: -400px -660px; }
-.emoji-1F58C { background-position: -420px -660px; }
-.emoji-1F58D { background-position: -440px -660px; }
-.emoji-1F590 { background-position: -460px -660px; }
-.emoji-1F590-1F3FB { background-position: -480px -660px; }
-.emoji-1F590-1F3FC { background-position: -500px -660px; }
-.emoji-1F590-1F3FD { background-position: -520px -660px; }
-.emoji-1F590-1F3FE { background-position: -540px -660px; }
-.emoji-1F590-1F3FF { background-position: -560px -660px; }
-.emoji-1F595 { background-position: -580px -660px; }
-.emoji-1F595-1F3FB { background-position: -600px -660px; }
-.emoji-1F595-1F3FC { background-position: -620px -660px; }
-.emoji-1F595-1F3FD { background-position: -640px -660px; }
-.emoji-1F595-1F3FE { background-position: -660px -660px; }
-.emoji-1F595-1F3FF { background-position: -680px 0; }
-.emoji-1F596 { background-position: -680px -20px; }
-.emoji-1F596-1F3FB { background-position: -680px -40px; }
-.emoji-1F596-1F3FC { background-position: -680px -60px; }
-.emoji-1F596-1F3FD { background-position: -680px -80px; }
-.emoji-1F596-1F3FE { background-position: -680px -100px; }
-.emoji-1F596-1F3FF { background-position: -680px -120px; }
-.emoji-1F5A4 { background-position: -680px -140px; }
-.emoji-1F5A5 { background-position: -680px -160px; }
-.emoji-1F5A8 { background-position: -680px -180px; }
-.emoji-1F5B1 { background-position: -680px -200px; }
-.emoji-1F5B2 { background-position: -680px -220px; }
-.emoji-1F5BC { background-position: -680px -240px; }
-.emoji-1F5C2 { background-position: -680px -260px; }
-.emoji-1F5C3 { background-position: -680px -280px; }
-.emoji-1F5C4 { background-position: -680px -300px; }
-.emoji-1F5D1 { background-position: -680px -320px; }
-.emoji-1F5D2 { background-position: -680px -340px; }
-.emoji-1F5D3 { background-position: -680px -360px; }
-.emoji-1F5DC { background-position: -680px -380px; }
-.emoji-1F5DD { background-position: -680px -400px; }
-.emoji-1F5DE { background-position: -680px -420px; }
-.emoji-1F5E1 { background-position: -680px -440px; }
-.emoji-1F5E3 { background-position: -680px -460px; }
-.emoji-1F5EF { background-position: -680px -480px; }
-.emoji-1F5F3 { background-position: -680px -500px; }
-.emoji-1F5FA { background-position: -680px -520px; }
-.emoji-1F5FB { background-position: -680px -540px; }
-.emoji-1F5FC { background-position: -680px -560px; }
-.emoji-1F5FD { background-position: -680px -580px; }
-.emoji-1F5FE { background-position: -680px -600px; }
-.emoji-1F5FF { background-position: -680px -620px; }
-.emoji-1F600 { background-position: -680px -640px; }
-.emoji-1F601 { background-position: -680px -660px; }
-.emoji-1F602 { background-position: 0 -680px; }
-.emoji-1F603 { background-position: -20px -680px; }
-.emoji-1F604 { background-position: -40px -680px; }
-.emoji-1F605 { background-position: -60px -680px; }
-.emoji-1F606 { background-position: -80px -680px; }
-.emoji-1F607 { background-position: -100px -680px; }
-.emoji-1F608 { background-position: -120px -680px; }
-.emoji-1F609 { background-position: -140px -680px; }
-.emoji-1F60A { background-position: -160px -680px; }
-.emoji-1F60B { background-position: -180px -680px; }
-.emoji-1F60C { background-position: -200px -680px; }
-.emoji-1F60D { background-position: -220px -680px; }
-.emoji-1F60E { background-position: -240px -680px; }
-.emoji-1F60F { background-position: -260px -680px; }
-.emoji-1F610 { background-position: -280px -680px; }
-.emoji-1F611 { background-position: -300px -680px; }
-.emoji-1F612 { background-position: -320px -680px; }
-.emoji-1F613 { background-position: -340px -680px; }
-.emoji-1F614 { background-position: -360px -680px; }
-.emoji-1F615 { background-position: -380px -680px; }
-.emoji-1F616 { background-position: -400px -680px; }
-.emoji-1F617 { background-position: -420px -680px; }
-.emoji-1F618 { background-position: -440px -680px; }
-.emoji-1F619 { background-position: -460px -680px; }
-.emoji-1F61A { background-position: -480px -680px; }
-.emoji-1F61B { background-position: -500px -680px; }
-.emoji-1F61C { background-position: -520px -680px; }
-.emoji-1F61D { background-position: -540px -680px; }
-.emoji-1F61E { background-position: -560px -680px; }
-.emoji-1F61F { background-position: -580px -680px; }
-.emoji-1F620 { background-position: -600px -680px; }
-.emoji-1F621 { background-position: -620px -680px; }
-.emoji-1F622 { background-position: -640px -680px; }
-.emoji-1F623 { background-position: -660px -680px; }
-.emoji-1F624 { background-position: -680px -680px; }
-.emoji-1F625 { background-position: -700px 0; }
-.emoji-1F626 { background-position: -700px -20px; }
-.emoji-1F627 { background-position: -700px -40px; }
-.emoji-1F628 { background-position: -700px -60px; }
-.emoji-1F629 { background-position: -700px -80px; }
-.emoji-1F62A { background-position: -700px -100px; }
-.emoji-1F62B { background-position: -700px -120px; }
-.emoji-1F62C { background-position: -700px -140px; }
-.emoji-1F62D { background-position: -700px -160px; }
-.emoji-1F62E { background-position: -700px -180px; }
-.emoji-1F62F { background-position: -700px -200px; }
-.emoji-1F630 { background-position: -700px -220px; }
-.emoji-1F631 { background-position: -700px -240px; }
-.emoji-1F632 { background-position: -700px -260px; }
-.emoji-1F633 { background-position: -700px -280px; }
-.emoji-1F634 { background-position: -700px -300px; }
-.emoji-1F635 { background-position: -700px -320px; }
-.emoji-1F636 { background-position: -700px -340px; }
-.emoji-1F637 { background-position: -700px -360px; }
-.emoji-1F638 { background-position: -700px -380px; }
-.emoji-1F639 { background-position: -700px -400px; }
-.emoji-1F63A { background-position: -700px -420px; }
-.emoji-1F63B { background-position: -700px -440px; }
-.emoji-1F63C { background-position: -700px -460px; }
-.emoji-1F63D { background-position: -700px -480px; }
-.emoji-1F63E { background-position: -700px -500px; }
-.emoji-1F63F { background-position: -700px -520px; }
-.emoji-1F640 { background-position: -700px -540px; }
-.emoji-1F641 { background-position: -700px -560px; }
-.emoji-1F642 { background-position: -700px -580px; }
-.emoji-1F643 { background-position: -700px -600px; }
-.emoji-1F644 { background-position: -700px -620px; }
-.emoji-1F645 { background-position: -700px -640px; }
-.emoji-1F645-1F3FB { background-position: -700px -660px; }
-.emoji-1F645-1F3FC { background-position: -700px -680px; }
-.emoji-1F645-1F3FD { background-position: 0 -700px; }
-.emoji-1F645-1F3FE { background-position: -20px -700px; }
-.emoji-1F645-1F3FF { background-position: -40px -700px; }
-.emoji-1F646 { background-position: -60px -700px; }
-.emoji-1F646-1F3FB { background-position: -80px -700px; }
-.emoji-1F646-1F3FC { background-position: -100px -700px; }
-.emoji-1F646-1F3FD { background-position: -120px -700px; }
-.emoji-1F646-1F3FE { background-position: -140px -700px; }
-.emoji-1F646-1F3FF { background-position: -160px -700px; }
-.emoji-1F647 { background-position: -180px -700px; }
-.emoji-1F647-1F3FB { background-position: -200px -700px; }
-.emoji-1F647-1F3FC { background-position: -220px -700px; }
-.emoji-1F647-1F3FD { background-position: -240px -700px; }
-.emoji-1F647-1F3FE { background-position: -260px -700px; }
-.emoji-1F647-1F3FF { background-position: -280px -700px; }
-.emoji-1F648 { background-position: -300px -700px; }
-.emoji-1F649 { background-position: -320px -700px; }
-.emoji-1F64A { background-position: -340px -700px; }
-.emoji-1F64B { background-position: -360px -700px; }
-.emoji-1F64B-1F3FB { background-position: -380px -700px; }
-.emoji-1F64B-1F3FC { background-position: -400px -700px; }
-.emoji-1F64B-1F3FD { background-position: -420px -700px; }
-.emoji-1F64B-1F3FE { background-position: -440px -700px; }
-.emoji-1F64B-1F3FF { background-position: -460px -700px; }
-.emoji-1F64C { background-position: -480px -700px; }
-.emoji-1F64C-1F3FB { background-position: -500px -700px; }
-.emoji-1F64C-1F3FC { background-position: -520px -700px; }
-.emoji-1F64C-1F3FD { background-position: -540px -700px; }
-.emoji-1F64C-1F3FE { background-position: -560px -700px; }
-.emoji-1F64C-1F3FF { background-position: -580px -700px; }
-.emoji-1F64D { background-position: -600px -700px; }
-.emoji-1F64D-1F3FB { background-position: -620px -700px; }
-.emoji-1F64D-1F3FC { background-position: -640px -700px; }
-.emoji-1F64D-1F3FD { background-position: -660px -700px; }
-.emoji-1F64D-1F3FE { background-position: -680px -700px; }
-.emoji-1F64D-1F3FF { background-position: -700px -700px; }
-.emoji-1F64E { background-position: -720px 0; }
-.emoji-1F64E-1F3FB { background-position: -720px -20px; }
-.emoji-1F64E-1F3FC { background-position: -720px -40px; }
-.emoji-1F64E-1F3FD { background-position: -720px -60px; }
-.emoji-1F64E-1F3FE { background-position: -720px -80px; }
-.emoji-1F64E-1F3FF { background-position: -720px -100px; }
-.emoji-1F64F { background-position: -720px -120px; }
-.emoji-1F64F-1F3FB { background-position: -720px -140px; }
-.emoji-1F64F-1F3FC { background-position: -720px -160px; }
-.emoji-1F64F-1F3FD { background-position: -720px -180px; }
-.emoji-1F64F-1F3FE { background-position: -720px -200px; }
-.emoji-1F64F-1F3FF { background-position: -720px -220px; }
-.emoji-1F680 { background-position: -720px -240px; }
-.emoji-1F681 { background-position: -720px -260px; }
-.emoji-1F682 { background-position: -720px -280px; }
-.emoji-1F683 { background-position: -720px -300px; }
-.emoji-1F684 { background-position: -720px -320px; }
-.emoji-1F685 { background-position: -720px -340px; }
-.emoji-1F686 { background-position: -720px -360px; }
-.emoji-1F687 { background-position: -720px -380px; }
-.emoji-1F688 { background-position: -720px -400px; }
-.emoji-1F689 { background-position: -720px -420px; }
-.emoji-1F68A { background-position: -720px -440px; }
-.emoji-1F68B { background-position: -720px -460px; }
-.emoji-1F68C { background-position: -720px -480px; }
-.emoji-1F68D { background-position: -720px -500px; }
-.emoji-1F68E { background-position: -720px -520px; }
-.emoji-1F68F { background-position: -720px -540px; }
-.emoji-1F690 { background-position: -720px -560px; }
-.emoji-1F691 { background-position: -720px -580px; }
-.emoji-1F692 { background-position: -720px -600px; }
-.emoji-1F693 { background-position: -720px -620px; }
-.emoji-1F694 { background-position: -720px -640px; }
-.emoji-1F695 { background-position: -720px -660px; }
-.emoji-1F696 { background-position: -720px -680px; }
-.emoji-1F697 { background-position: -720px -700px; }
-.emoji-1F698 { background-position: 0 -720px; }
-.emoji-1F699 { background-position: -20px -720px; }
-.emoji-1F69A { background-position: -40px -720px; }
-.emoji-1F69B { background-position: -60px -720px; }
-.emoji-1F69C { background-position: -80px -720px; }
-.emoji-1F69D { background-position: -100px -720px; }
-.emoji-1F69E { background-position: -120px -720px; }
-.emoji-1F69F { background-position: -140px -720px; }
-.emoji-1F6A0 { background-position: -160px -720px; }
-.emoji-1F6A1 { background-position: -180px -720px; }
-.emoji-1F6A2 { background-position: -200px -720px; }
-.emoji-1F6A3 { background-position: -220px -720px; }
-.emoji-1F6A3-1F3FB { background-position: -240px -720px; }
-.emoji-1F6A3-1F3FC { background-position: -260px -720px; }
-.emoji-1F6A3-1F3FD { background-position: -280px -720px; }
-.emoji-1F6A3-1F3FE { background-position: -300px -720px; }
-.emoji-1F6A3-1F3FF { background-position: -320px -720px; }
-.emoji-1F6A4 { background-position: -340px -720px; }
-.emoji-1F6A5 { background-position: -360px -720px; }
-.emoji-1F6A6 { background-position: -380px -720px; }
-.emoji-1F6A7 { background-position: -400px -720px; }
-.emoji-1F6A8 { background-position: -420px -720px; }
-.emoji-1F6A9 { background-position: -440px -720px; }
-.emoji-1F6AA { background-position: -460px -720px; }
-.emoji-1F6AB { background-position: -480px -720px; }
-.emoji-1F6AC { background-position: -500px -720px; }
-.emoji-1F6AD { background-position: -520px -720px; }
-.emoji-1F6AE { background-position: -540px -720px; }
-.emoji-1F6AF { background-position: -560px -720px; }
-.emoji-1F6B0 { background-position: -580px -720px; }
-.emoji-1F6B1 { background-position: -600px -720px; }
-.emoji-1F6B2 { background-position: -620px -720px; }
-.emoji-1F6B3 { background-position: -640px -720px; }
-.emoji-1F6B4 { background-position: -660px -720px; }
-.emoji-1F6B4-1F3FB { background-position: -680px -720px; }
-.emoji-1F6B4-1F3FC { background-position: -700px -720px; }
-.emoji-1F6B4-1F3FD { background-position: -720px -720px; }
-.emoji-1F6B4-1F3FE { background-position: -740px 0; }
-.emoji-1F6B4-1F3FF { background-position: -740px -20px; }
-.emoji-1F6B5 { background-position: -740px -40px; }
-.emoji-1F6B5-1F3FB { background-position: -740px -60px; }
-.emoji-1F6B5-1F3FC { background-position: -740px -80px; }
-.emoji-1F6B5-1F3FD { background-position: -740px -100px; }
-.emoji-1F6B5-1F3FE { background-position: -740px -120px; }
-.emoji-1F6B5-1F3FF { background-position: -740px -140px; }
-.emoji-1F6B6 { background-position: -740px -160px; }
-.emoji-1F6B6-1F3FB { background-position: -740px -180px; }
-.emoji-1F6B6-1F3FC { background-position: -740px -200px; }
-.emoji-1F6B6-1F3FD { background-position: -740px -220px; }
-.emoji-1F6B6-1F3FE { background-position: -740px -240px; }
-.emoji-1F6B6-1F3FF { background-position: -740px -260px; }
-.emoji-1F6B7 { background-position: -740px -280px; }
-.emoji-1F6B8 { background-position: -740px -300px; }
-.emoji-1F6B9 { background-position: -740px -320px; }
-.emoji-1F6BA { background-position: -740px -340px; }
-.emoji-1F6BB { background-position: -740px -360px; }
-.emoji-1F6BC { background-position: -740px -380px; }
-.emoji-1F6BD { background-position: -740px -400px; }
-.emoji-1F6BE { background-position: -740px -420px; }
-.emoji-1F6BF { background-position: -740px -440px; }
-.emoji-1F6C0 { background-position: -740px -460px; }
-.emoji-1F6C0-1F3FB { background-position: -740px -480px; }
-.emoji-1F6C0-1F3FC { background-position: -740px -500px; }
-.emoji-1F6C0-1F3FD { background-position: -740px -520px; }
-.emoji-1F6C0-1F3FE { background-position: -740px -540px; }
-.emoji-1F6C0-1F3FF { background-position: -740px -560px; }
-.emoji-1F6C1 { background-position: -740px -580px; }
-.emoji-1F6C2 { background-position: -740px -600px; }
-.emoji-1F6C3 { background-position: -740px -620px; }
-.emoji-1F6C4 { background-position: -740px -640px; }
-.emoji-1F6C5 { background-position: -740px -660px; }
-.emoji-1F6CB { background-position: -740px -680px; }
-.emoji-1F6CC { background-position: -740px -700px; }
-.emoji-1F6CD { background-position: -740px -720px; }
-.emoji-1F6CE { background-position: 0 -740px; }
-.emoji-1F6CF { background-position: -20px -740px; }
-.emoji-1F6D0 { background-position: -40px -740px; }
-.emoji-1F6D1 { background-position: -60px -740px; }
-.emoji-1F6D2 { background-position: -80px -740px; }
-.emoji-1F6E0 { background-position: -100px -740px; }
-.emoji-1F6E1 { background-position: -120px -740px; }
-.emoji-1F6E2 { background-position: -140px -740px; }
-.emoji-1F6E3 { background-position: -160px -740px; }
-.emoji-1F6E4 { background-position: -180px -740px; }
-.emoji-1F6E5 { background-position: -200px -740px; }
-.emoji-1F6E9 { background-position: -220px -740px; }
-.emoji-1F6EB { background-position: -240px -740px; }
-.emoji-1F6EC { background-position: -260px -740px; }
-.emoji-1F6F0 { background-position: -280px -740px; }
-.emoji-1F6F3 { background-position: -300px -740px; }
-.emoji-1F6F4 { background-position: -320px -740px; }
-.emoji-1F6F5 { background-position: -340px -740px; }
-.emoji-1F6F6 { background-position: -360px -740px; }
-.emoji-1F910 { background-position: -380px -740px; }
-.emoji-1F911 { background-position: -400px -740px; }
-.emoji-1F912 { background-position: -420px -740px; }
-.emoji-1F913 { background-position: -440px -740px; }
-.emoji-1F914 { background-position: -460px -740px; }
-.emoji-1F915 { background-position: -480px -740px; }
-.emoji-1F916 { background-position: -500px -740px; }
-.emoji-1F917 { background-position: -520px -740px; }
-.emoji-1F918 { background-position: -540px -740px; }
-.emoji-1F918-1F3FB { background-position: -560px -740px; }
-.emoji-1F918-1F3FC { background-position: -580px -740px; }
-.emoji-1F918-1F3FD { background-position: -600px -740px; }
-.emoji-1F918-1F3FE { background-position: -620px -740px; }
-.emoji-1F918-1F3FF { background-position: -640px -740px; }
-.emoji-1F919 { background-position: -660px -740px; }
-.emoji-1F919-1F3FB { background-position: -680px -740px; }
-.emoji-1F919-1F3FC { background-position: -700px -740px; }
-.emoji-1F919-1F3FD { background-position: -720px -740px; }
-.emoji-1F919-1F3FE { background-position: -740px -740px; }
-.emoji-1F919-1F3FF { background-position: -760px 0; }
-.emoji-1F91A { background-position: -760px -20px; }
-.emoji-1F91A-1F3FB { background-position: -760px -40px; }
-.emoji-1F91A-1F3FC { background-position: -760px -60px; }
-.emoji-1F91A-1F3FD { background-position: -760px -80px; }
-.emoji-1F91A-1F3FE { background-position: -760px -100px; }
-.emoji-1F91A-1F3FF { background-position: -760px -120px; }
-.emoji-1F91B { background-position: -760px -140px; }
-.emoji-1F91B-1F3FB { background-position: -760px -160px; }
-.emoji-1F91B-1F3FC { background-position: -760px -180px; }
-.emoji-1F91B-1F3FD { background-position: -760px -200px; }
-.emoji-1F91B-1F3FE { background-position: -760px -220px; }
-.emoji-1F91B-1F3FF { background-position: -760px -240px; }
-.emoji-1F91C { background-position: -760px -260px; }
-.emoji-1F91C-1F3FB { background-position: -760px -280px; }
-.emoji-1F91C-1F3FC { background-position: -760px -300px; }
-.emoji-1F91C-1F3FD { background-position: -760px -320px; }
-.emoji-1F91C-1F3FE { background-position: -760px -340px; }
-.emoji-1F91C-1F3FF { background-position: -760px -360px; }
-.emoji-1F91D { background-position: -760px -380px; }
-.emoji-1F91D-1F3FB { background-position: -760px -400px; }
-.emoji-1F91D-1F3FC { background-position: -760px -420px; }
-.emoji-1F91D-1F3FD { background-position: -760px -440px; }
-.emoji-1F91D-1F3FE { background-position: -760px -460px; }
-.emoji-1F91D-1F3FF { background-position: -760px -480px; }
-.emoji-1F91E { background-position: -760px -500px; }
-.emoji-1F91E-1F3FB { background-position: -760px -520px; }
-.emoji-1F91E-1F3FC { background-position: -760px -540px; }
-.emoji-1F91E-1F3FD { background-position: -760px -560px; }
-.emoji-1F91E-1F3FE { background-position: -760px -580px; }
-.emoji-1F91E-1F3FF { background-position: -760px -600px; }
-.emoji-1F920 { background-position: -760px -620px; }
-.emoji-1F921 { background-position: -760px -640px; }
-.emoji-1F922 { background-position: -760px -660px; }
-.emoji-1F923 { background-position: -760px -680px; }
-.emoji-1F924 { background-position: -760px -700px; }
-.emoji-1F925 { background-position: -760px -720px; }
-.emoji-1F926 { background-position: -760px -740px; }
-.emoji-1F926-1F3FB { background-position: 0 -760px; }
-.emoji-1F926-1F3FC { background-position: -20px -760px; }
-.emoji-1F926-1F3FD { background-position: -40px -760px; }
-.emoji-1F926-1F3FE { background-position: -60px -760px; }
-.emoji-1F926-1F3FF { background-position: -80px -760px; }
-.emoji-1F927 { background-position: -100px -760px; }
-.emoji-1F930 { background-position: -120px -760px; }
-.emoji-1F930-1F3FB { background-position: -140px -760px; }
-.emoji-1F930-1F3FC { background-position: -160px -760px; }
-.emoji-1F930-1F3FD { background-position: -180px -760px; }
-.emoji-1F930-1F3FE { background-position: -200px -760px; }
-.emoji-1F930-1F3FF { background-position: -220px -760px; }
-.emoji-1F933 { background-position: -240px -760px; }
-.emoji-1F933-1F3FB { background-position: -260px -760px; }
-.emoji-1F933-1F3FC { background-position: -280px -760px; }
-.emoji-1F933-1F3FD { background-position: -300px -760px; }
-.emoji-1F933-1F3FE { background-position: -320px -760px; }
-.emoji-1F933-1F3FF { background-position: -340px -760px; }
-.emoji-1F934 { background-position: -360px -760px; }
-.emoji-1F934-1F3FB { background-position: -380px -760px; }
-.emoji-1F934-1F3FC { background-position: -400px -760px; }
-.emoji-1F934-1F3FD { background-position: -420px -760px; }
-.emoji-1F934-1F3FE { background-position: -440px -760px; }
-.emoji-1F934-1F3FF { background-position: -460px -760px; }
-.emoji-1F935 { background-position: -480px -760px; }
-.emoji-1F935-1F3FB { background-position: -500px -760px; }
-.emoji-1F935-1F3FC { background-position: -520px -760px; }
-.emoji-1F935-1F3FD { background-position: -540px -760px; }
-.emoji-1F935-1F3FE { background-position: -560px -760px; }
-.emoji-1F935-1F3FF { background-position: -580px -760px; }
-.emoji-1F936 { background-position: -600px -760px; }
-.emoji-1F936-1F3FB { background-position: -620px -760px; }
-.emoji-1F936-1F3FC { background-position: -640px -760px; }
-.emoji-1F936-1F3FD { background-position: -660px -760px; }
-.emoji-1F936-1F3FE { background-position: -680px -760px; }
-.emoji-1F936-1F3FF { background-position: -700px -760px; }
-.emoji-1F937 { background-position: -720px -760px; }
-.emoji-1F937-1F3FB { background-position: -740px -760px; }
-.emoji-1F937-1F3FC { background-position: -760px -760px; }
-.emoji-1F937-1F3FD { background-position: -780px 0; }
-.emoji-1F937-1F3FE { background-position: -780px -20px; }
-.emoji-1F937-1F3FF { background-position: -780px -40px; }
-.emoji-1F938 { background-position: -780px -60px; }
-.emoji-1F938-1F3FB { background-position: -780px -80px; }
-.emoji-1F938-1F3FC { background-position: -780px -100px; }
-.emoji-1F938-1F3FD { background-position: -780px -120px; }
-.emoji-1F938-1F3FE { background-position: -780px -140px; }
-.emoji-1F938-1F3FF { background-position: -780px -160px; }
-.emoji-1F939 { background-position: -780px -180px; }
-.emoji-1F939-1F3FB { background-position: -780px -200px; }
-.emoji-1F939-1F3FC { background-position: -780px -220px; }
-.emoji-1F939-1F3FD { background-position: -780px -240px; }
-.emoji-1F939-1F3FE { background-position: -780px -260px; }
-.emoji-1F939-1F3FF { background-position: -780px -280px; }
-.emoji-1F93A { background-position: -780px -300px; }
-.emoji-1F93C { background-position: -780px -320px; }
-.emoji-1F93C-1F3FB { background-position: -780px -340px; }
-.emoji-1F93C-1F3FC { background-position: -780px -360px; }
-.emoji-1F93C-1F3FD { background-position: -780px -380px; }
-.emoji-1F93C-1F3FE { background-position: -780px -400px; }
-.emoji-1F93C-1F3FF { background-position: -780px -420px; }
-.emoji-1F93D { background-position: -780px -440px; }
-.emoji-1F93D-1F3FB { background-position: -780px -460px; }
-.emoji-1F93D-1F3FC { background-position: -780px -480px; }
-.emoji-1F93D-1F3FD { background-position: -780px -500px; }
-.emoji-1F93D-1F3FE { background-position: -780px -520px; }
-.emoji-1F93D-1F3FF { background-position: -780px -540px; }
-.emoji-1F93E { background-position: -780px -560px; }
-.emoji-1F93E-1F3FB { background-position: -780px -580px; }
-.emoji-1F93E-1F3FC { background-position: -780px -600px; }
-.emoji-1F93E-1F3FD { background-position: -780px -620px; }
-.emoji-1F93E-1F3FE { background-position: -780px -640px; }
-.emoji-1F93E-1F3FF { background-position: -780px -660px; }
-.emoji-1F940 { background-position: -780px -680px; }
-.emoji-1F941 { background-position: -780px -700px; }
-.emoji-1F942 { background-position: -780px -720px; }
-.emoji-1F943 { background-position: -780px -740px; }
-.emoji-1F944 { background-position: -780px -760px; }
-.emoji-1F945 { background-position: 0 -780px; }
-.emoji-1F947 { background-position: -20px -780px; }
-.emoji-1F948 { background-position: -40px -780px; }
-.emoji-1F949 { background-position: -60px -780px; }
-.emoji-1F94A { background-position: -80px -780px; }
-.emoji-1F94B { background-position: -100px -780px; }
-.emoji-1F950 { background-position: -120px -780px; }
-.emoji-1F951 { background-position: -140px -780px; }
-.emoji-1F952 { background-position: -160px -780px; }
-.emoji-1F953 { background-position: -180px -780px; }
-.emoji-1F954 { background-position: -200px -780px; }
-.emoji-1F955 { background-position: -220px -780px; }
-.emoji-1F956 { background-position: -240px -780px; }
-.emoji-1F957 { background-position: -260px -780px; }
-.emoji-1F958 { background-position: -280px -780px; }
-.emoji-1F959 { background-position: -300px -780px; }
-.emoji-1F95A { background-position: -320px -780px; }
-.emoji-1F95B { background-position: -340px -780px; }
-.emoji-1F95C { background-position: -360px -780px; }
-.emoji-1F95D { background-position: -380px -780px; }
-.emoji-1F95E { background-position: -400px -780px; }
-.emoji-1F980 { background-position: -420px -780px; }
-.emoji-1F981 { background-position: -440px -780px; }
-.emoji-1F982 { background-position: -460px -780px; }
-.emoji-1F983 { background-position: -480px -780px; }
-.emoji-1F984 { background-position: -500px -780px; }
-.emoji-1F985 { background-position: -520px -780px; }
-.emoji-1F986 { background-position: -540px -780px; }
-.emoji-1F987 { background-position: -560px -780px; }
-.emoji-1F988 { background-position: -580px -780px; }
-.emoji-1F989 { background-position: -600px -780px; }
-.emoji-1F98A { background-position: -620px -780px; }
-.emoji-1F98B { background-position: -640px -780px; }
-.emoji-1F98C { background-position: -660px -780px; }
-.emoji-1F98D { background-position: -680px -780px; }
-.emoji-1F98E { background-position: -700px -780px; }
-.emoji-1F98F { background-position: -720px -780px; }
-.emoji-1F990 { background-position: -740px -780px; }
-.emoji-1F991 { background-position: -760px -780px; }
-.emoji-1F9C0 { background-position: -780px -780px; }
-.emoji-203C { background-position: -800px 0; }
-.emoji-2049 { background-position: -800px -20px; }
-.emoji-2122 { background-position: -800px -40px; }
-.emoji-2139 { background-position: -800px -60px; }
-.emoji-2194 { background-position: -800px -80px; }
-.emoji-2195 { background-position: -800px -100px; }
-.emoji-2196 { background-position: -800px -120px; }
-.emoji-2197 { background-position: -800px -140px; }
-.emoji-2198 { background-position: -800px -160px; }
-.emoji-2199 { background-position: -800px -180px; }
-.emoji-21A9 { background-position: -800px -200px; }
-.emoji-21AA { background-position: -800px -220px; }
-.emoji-231A { background-position: -800px -240px; }
-.emoji-231B { background-position: -800px -260px; }
-.emoji-2328 { background-position: -800px -280px; }
-.emoji-23CF { background-position: -800px -300px; }
-.emoji-23E9 { background-position: -800px -320px; }
-.emoji-23EA { background-position: -800px -340px; }
-.emoji-23EB { background-position: -800px -360px; }
-.emoji-23EC { background-position: -800px -380px; }
-.emoji-23ED { background-position: -800px -400px; }
-.emoji-23EE { background-position: -800px -420px; }
-.emoji-23EF { background-position: -800px -440px; }
-.emoji-23F0 { background-position: -800px -460px; }
-.emoji-23F1 { background-position: -800px -480px; }
-.emoji-23F2 { background-position: -800px -500px; }
-.emoji-23F3 { background-position: -800px -520px; }
-.emoji-23F8 { background-position: -800px -540px; }
-.emoji-23F9 { background-position: -800px -560px; }
-.emoji-23FA { background-position: -800px -580px; }
-.emoji-24C2 { background-position: -800px -600px; }
-.emoji-25AA { background-position: -800px -620px; }
-.emoji-25AB { background-position: -800px -640px; }
-.emoji-25B6 { background-position: -800px -660px; }
-.emoji-25C0 { background-position: -800px -680px; }
-.emoji-25FB { background-position: -800px -700px; }
-.emoji-25FC { background-position: -800px -720px; }
-.emoji-25FD { background-position: -800px -740px; }
-.emoji-25FE { background-position: -800px -760px; }
-.emoji-2600 { background-position: -800px -780px; }
-.emoji-2601 { background-position: 0 -800px; }
-.emoji-2602 { background-position: -20px -800px; }
-.emoji-2603 { background-position: -40px -800px; }
-.emoji-2604 { background-position: -60px -800px; }
-.emoji-260E { background-position: -80px -800px; }
-.emoji-2611 { background-position: -100px -800px; }
-.emoji-2614 { background-position: -120px -800px; }
-.emoji-2615 { background-position: -140px -800px; }
-.emoji-2618 { background-position: -160px -800px; }
-.emoji-261D { background-position: -180px -800px; }
-.emoji-261D-1F3FB { background-position: -200px -800px; }
-.emoji-261D-1F3FC { background-position: -220px -800px; }
-.emoji-261D-1F3FD { background-position: -240px -800px; }
-.emoji-261D-1F3FE { background-position: -260px -800px; }
-.emoji-261D-1F3FF { background-position: -280px -800px; }
-.emoji-2620 { background-position: -300px -800px; }
-.emoji-2622 { background-position: -320px -800px; }
-.emoji-2623 { background-position: -340px -800px; }
-.emoji-2626 { background-position: -360px -800px; }
-.emoji-262A { background-position: -380px -800px; }
-.emoji-262E { background-position: -400px -800px; }
-.emoji-262F { background-position: -420px -800px; }
-.emoji-2638 { background-position: -440px -800px; }
-.emoji-2639 { background-position: -460px -800px; }
-.emoji-263A { background-position: -480px -800px; }
-.emoji-2648 { background-position: -500px -800px; }
-.emoji-2649 { background-position: -520px -800px; }
-.emoji-264A { background-position: -540px -800px; }
-.emoji-264B { background-position: -560px -800px; }
-.emoji-264C { background-position: -580px -800px; }
-.emoji-264D { background-position: -600px -800px; }
-.emoji-264E { background-position: -620px -800px; }
-.emoji-264F { background-position: -640px -800px; }
-.emoji-2650 { background-position: -660px -800px; }
-.emoji-2651 { background-position: -680px -800px; }
-.emoji-2652 { background-position: -700px -800px; }
-.emoji-2653 { background-position: -720px -800px; }
-.emoji-2660 { background-position: -740px -800px; }
-.emoji-2663 { background-position: -760px -800px; }
-.emoji-2665 { background-position: -780px -800px; }
-.emoji-2666 { background-position: -800px -800px; }
-.emoji-2668 { background-position: -820px 0; }
-.emoji-267B { background-position: -820px -20px; }
-.emoji-267F { background-position: -820px -40px; }
-.emoji-2692 { background-position: -820px -60px; }
-.emoji-2693 { background-position: -820px -80px; }
-.emoji-2694 { background-position: -820px -100px; }
-.emoji-2696 { background-position: -820px -120px; }
-.emoji-2697 { background-position: -820px -140px; }
-.emoji-2699 { background-position: -820px -160px; }
-.emoji-269B { background-position: -820px -180px; }
-.emoji-269C { background-position: -820px -200px; }
-.emoji-26A0 { background-position: -820px -220px; }
-.emoji-26A1 { background-position: -820px -240px; }
-.emoji-26AA { background-position: -820px -260px; }
-.emoji-26AB { background-position: -820px -280px; }
-.emoji-26B0 { background-position: -820px -300px; }
-.emoji-26B1 { background-position: -820px -320px; }
-.emoji-26BD { background-position: -820px -340px; }
-.emoji-26BE { background-position: -820px -360px; }
-.emoji-26C4 { background-position: -820px -380px; }
-.emoji-26C5 { background-position: -820px -400px; }
-.emoji-26C8 { background-position: -820px -420px; }
-.emoji-26CE { background-position: -820px -440px; }
-.emoji-26CF { background-position: -820px -460px; }
-.emoji-26D1 { background-position: -820px -480px; }
-.emoji-26D3 { background-position: -820px -500px; }
-.emoji-26D4 { background-position: -820px -520px; }
-.emoji-26E9 { background-position: -820px -540px; }
-.emoji-26EA { background-position: -820px -560px; }
-.emoji-26F0 { background-position: -820px -580px; }
-.emoji-26F1 { background-position: -820px -600px; }
-.emoji-26F2 { background-position: -820px -620px; }
-.emoji-26F3 { background-position: -820px -640px; }
-.emoji-26F4 { background-position: -820px -660px; }
-.emoji-26F5 { background-position: -820px -680px; }
-.emoji-26F7 { background-position: -820px -700px; }
-.emoji-26F8 { background-position: -820px -720px; }
-.emoji-26F9 { background-position: -820px -740px; }
-.emoji-26F9-1F3FB { background-position: -820px -760px; }
-.emoji-26F9-1F3FC { background-position: -820px -780px; }
-.emoji-26F9-1F3FD { background-position: -820px -800px; }
-.emoji-26F9-1F3FE { background-position: 0 -820px; }
-.emoji-26F9-1F3FF { background-position: -20px -820px; }
-.emoji-26FA { background-position: -40px -820px; }
-.emoji-26FD { background-position: -60px -820px; }
-.emoji-2702 { background-position: -80px -820px; }
-.emoji-2705 { background-position: -100px -820px; }
-.emoji-2708 { background-position: -120px -820px; }
-.emoji-2709 { background-position: -140px -820px; }
-.emoji-270A { background-position: -160px -820px; }
-.emoji-270A-1F3FB { background-position: -180px -820px; }
-.emoji-270A-1F3FC { background-position: -200px -820px; }
-.emoji-270A-1F3FD { background-position: -220px -820px; }
-.emoji-270A-1F3FE { background-position: -240px -820px; }
-.emoji-270A-1F3FF { background-position: -260px -820px; }
-.emoji-270B { background-position: -280px -820px; }
-.emoji-270B-1F3FB { background-position: -300px -820px; }
-.emoji-270B-1F3FC { background-position: -320px -820px; }
-.emoji-270B-1F3FD { background-position: -340px -820px; }
-.emoji-270B-1F3FE { background-position: -360px -820px; }
-.emoji-270B-1F3FF { background-position: -380px -820px; }
-.emoji-270C { background-position: -400px -820px; }
-.emoji-270C-1F3FB { background-position: -420px -820px; }
-.emoji-270C-1F3FC { background-position: -440px -820px; }
-.emoji-270C-1F3FD { background-position: -460px -820px; }
-.emoji-270C-1F3FE { background-position: -480px -820px; }
-.emoji-270C-1F3FF { background-position: -500px -820px; }
-.emoji-270D { background-position: -520px -820px; }
-.emoji-270D-1F3FB { background-position: -540px -820px; }
-.emoji-270D-1F3FC { background-position: -560px -820px; }
-.emoji-270D-1F3FD { background-position: -580px -820px; }
-.emoji-270D-1F3FE { background-position: -600px -820px; }
-.emoji-270D-1F3FF { background-position: -620px -820px; }
-.emoji-270F { background-position: -640px -820px; }
-.emoji-2712 { background-position: -660px -820px; }
-.emoji-2714 { background-position: -680px -820px; }
-.emoji-2716 { background-position: -700px -820px; }
-.emoji-271D { background-position: -720px -820px; }
-.emoji-2721 { background-position: -740px -820px; }
-.emoji-2728 { background-position: -760px -820px; }
-.emoji-2733 { background-position: -780px -820px; }
-.emoji-2734 { background-position: -800px -820px; }
-.emoji-2744 { background-position: -820px -820px; }
-.emoji-2747 { background-position: -840px 0; }
-.emoji-274C { background-position: -840px -20px; }
-.emoji-274E { background-position: -840px -40px; }
-.emoji-2753 { background-position: -840px -60px; }
-.emoji-2754 { background-position: -840px -80px; }
-.emoji-2755 { background-position: -840px -100px; }
-.emoji-2757 { background-position: -840px -120px; }
-.emoji-2763 { background-position: -840px -140px; }
-.emoji-2764 { background-position: -840px -160px; }
-.emoji-2795 { background-position: -840px -180px; }
-.emoji-2796 { background-position: -840px -200px; }
-.emoji-2797 { background-position: -840px -220px; }
-.emoji-27A1 { background-position: -840px -240px; }
-.emoji-27B0 { background-position: -840px -260px; }
-.emoji-27BF { background-position: -840px -280px; }
-.emoji-2934 { background-position: -840px -300px; }
-.emoji-2935 { background-position: -840px -320px; }
-.emoji-2B05 { background-position: -840px -340px; }
-.emoji-2B06 { background-position: -840px -360px; }
-.emoji-2B07 { background-position: -840px -380px; }
-.emoji-2B1B { background-position: -840px -400px; }
-.emoji-2B1C { background-position: -840px -420px; }
-.emoji-2B50 { background-position: -840px -440px; }
-.emoji-2B55 { background-position: -840px -460px; }
-.emoji-3030 { background-position: -840px -480px; }
-.emoji-303D { background-position: -840px -500px; }
-.emoji-3297 { background-position: -840px -520px; }
-.emoji-3299 { background-position: -840px -540px; }
-
-.emoji-icon {
- background-image: image-url('emoji.png');
- background-repeat: no-repeat;
- height: 20px;
- width: 20px;
-
- @media only screen and (-webkit-min-device-pixel-ratio: 2),
- only screen and (min--moz-device-pixel-ratio: 2),
- only screen and (-o-min-device-pixel-ratio: 2/1),
- only screen and (min-device-pixel-ratio: 2),
- only screen and (min-resolution: 192dpi),
- only screen and (min-resolution: 2dppx) {
- background-image: image-url('emoji@2x.png');
- background-size: 860px 840px;
- }
-}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index fc49ff780fc..3d60426de01 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -1,10 +1,56 @@
-.environments-container,
-.deployments-container {
+@media (max-width: $screen-md-max) {
+ .deployments-container {
+ width: 100%;
+ overflow: auto;
+ }
+}
+
+.environments-list-loading {
width: 100%;
- overflow: auto;
+ font-size: 34px;
+}
+
+@media (max-width: $screen-xs-max) {
+ .environments-container {
+ width: 100%;
+ overflow: auto;
+ }
}
.environments {
+ table-layout: fixed;
+
+ .environments-commit,
+ .environments-actions,
+ .environments-deploy,
+ .environments-build,
+ .environments-date {
+ position: static;
+ float: none;
+ display: table-cell;
+ }
+
+ .environments-name,
+ .environments-commit,
+ .environments-actions {
+ width: 20%;
+ }
+
+ .environments-date {
+ width: 10%;
+ }
+
+ .environments-deploy,
+ .environments-build {
+ width: 15%;
+ }
+
+ .environment-name,
+ .environments-build-cell,
+ .deployment-column {
+ word-break: break-all;
+ }
+
.deployment-column {
.avatar {
float: none;
@@ -15,6 +61,10 @@
margin: 0;
}
+ .avatar-image-container {
+ text-decoration: none;
+ }
+
.icon-play {
height: 13px;
width: 12px;
@@ -22,14 +72,14 @@
.external-url,
.dropdown-new {
- color: $table-text-gray;
+ color: $gl-gray-light;
}
.dropdown-menu {
.fa {
margin-right: 6px;
- color: $table-text-gray;
+ color: $gl-gray-light;
}
}
@@ -38,8 +88,9 @@
color: $gl-dark-link-color;
}
- .stop-env-link {
- color: $table-text-gray;
+ .stop-env-link,
+ .external-url {
+ color: $gl-gray-light;
.stop-env-icon {
font-size: 14px;
@@ -58,10 +109,29 @@
}
}
}
+
+ .children-row .environment-name {
+ margin-left: 17px;
+ margin-right: -17px;
+ }
+
+ .folder-icon {
+ padding: 0 5px 0 0;
+ }
+
+ .folder-name {
+ cursor: pointer;
+
+ .badge {
+ font-weight: normal;
+ background-color: $gray-darker;
+ color: $gl-gray-light;
+ vertical-align: baseline;
+ }
+ }
}
.table.ci-table.environments {
-
.icon-container {
width: 20px;
text-align: center;
diff --git a/app/assets/stylesheets/pages/errors.scss b/app/assets/stylesheets/pages/errors.scss
deleted file mode 100644
index 11309817d31..00000000000
--- a/app/assets/stylesheets/pages/errors.scss
+++ /dev/null
@@ -1,16 +0,0 @@
-.error-page {
- max-width: 400px;
- margin: 0 auto;
-
- h1,
- h2,
- h3 {
- text-align: center;
- }
-
- h1 {
- font-size: 56px;
- line-height: 100px;
- font-weight: 300;
- }
-}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 3004959ff7b..98925c2d0cb 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -5,7 +5,7 @@
.event-item {
font-size: $gl-font-size;
padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top);
- border-bottom: 1px solid $table-border-color;
+ border-bottom: 1px solid $white-normal;
color: $list-text-color;
&.event-inline {
@@ -62,7 +62,7 @@
border: none;
background: $gray-light;
border-radius: 0;
- color: #777;
+ color: $events-pre-color;
margin: 0 20px;
overflow: hidden;
}
@@ -80,7 +80,7 @@
}
.event-note-icon {
- color: #777;
+ color: $events-pre-color;
float: left;
font-size: $gl-font-size;
line-height: 16px;
@@ -91,7 +91,7 @@
.event_icon {
position: relative;
float: right;
- border: 1px solid #eee;
+ border: 1px solid $gray-darker;
padding: 5px;
border-radius: 5px;
background: $gray-light;
@@ -170,7 +170,7 @@
.event-body {
margin: 0;
- border-left: 2px solid #ddd;
+ border-left: 2px solid $events-body-border;
padding-left: 10px;
}
@@ -186,4 +186,3 @@
display: none;
}
}
-
diff --git a/app/assets/stylesheets/pages/explore.scss b/app/assets/stylesheets/pages/explore.scss
deleted file mode 100644
index 9b92128624c..00000000000
--- a/app/assets/stylesheets/pages/explore.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-.explore-title {
- text-align: center;
-
- h3 {
- font-weight: normal;
- font-size: 30px;
- }
-}
diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss
index f7f9a9bb770..84da9180f93 100644
--- a/app/assets/stylesheets/pages/graph.scss
+++ b/app/assets/stylesheets/pages/graph.scss
@@ -2,15 +2,15 @@
border: 1px solid $border-color;
.controls {
- color: #888;
+ color: $project-network-controls-color;
font-size: 14px;
padding: 5px;
border-bottom: 1px solid $border-color;
- background: #eee;
+ background: $gray-darker;
}
.network-graph {
- background: #fff;
+ background: $white-light;
height: 500px;
overflow-y: scroll;
overflow-x: hidden;
@@ -20,15 +20,14 @@
.graphs {
.graph-author-email {
float: right;
- color: #777;
+ color: $graph-author-email-color;
}
.graph-additions {
- color: #4a2;
+ color: $gl-text-green;
}
.graph-deletions {
- color: #d12f19;
+ color: $gl-text-red;
}
}
-
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 4375e29c8db..a9af7af59e2 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -10,7 +10,6 @@
}
.group-row {
-
.stats {
float: right;
line-height: $list-text-height;
@@ -23,36 +22,18 @@
}
.ldap-group-links {
-
.form-actions {
margin-bottom: $gl-padding;
}
}
-.groups-cover-block {
-
- .container-fluid {
- position: relative;
- }
-
- .group-right-buttons {
- position: absolute;
- right: 16px;
-
- .btn {
- @include btn-gray;
- padding: 3px 10px;
- background-color: $background-color;
- }
- }
-
- .group-avatar {
- border: 0;
+.group-buttons {
+ .notification-dropdown {
+ display: inline-block;
}
}
.groups-header {
-
@media (min-width: $screen-sm-min) {
.nav-links {
width: 35%;
@@ -68,14 +49,14 @@
padding: 50px 100px;
overflow: hidden;
- @media (max-width: $screen-md-min) {
+ @media (max-width: $screen-sm-max) {
padding: 50px 0;
}
svg {
float: right;
- @media (max-width: $screen-md-min) {
+ @media (max-width: $screen-sm-max) {
float: none;
display: block;
width: 250px;
@@ -90,7 +71,7 @@
width: 460px;
margin-top: 120px;
- @media (max-width: $screen-md-min) {
+ @media (max-width: $screen-sm-max) {
float: none;
margin-top: 60px;
width: auto;
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index a48b4c65db8..dae8ccdef6c 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -9,7 +9,7 @@
li {
line-height: 24px;
- color: #888;
+ color: $document-index-color;
a {
margin-right: 3px;
@@ -20,7 +20,7 @@
.shortcut-mappings {
font-size: 12px;
- color: #555;
+ color: $help-shortcut-mapping-color;
tbody:first-child tr:first-child {
padding-top: 0;
@@ -29,7 +29,7 @@
th {
padding-top: 15px;
line-height: 1.5;
- color: #333;
+ color: $help-shortcut-header-color;
text-align: left;
}
@@ -42,7 +42,7 @@
.shortcut {
padding-right: 10px;
- color: #999;
+ color: $help-shortcut-color;
text-align: right;
white-space: nowrap;
}
@@ -60,7 +60,7 @@
// Border around images in the help pages.
img:not(.emoji) {
- border: 1px solid $table-border-gray;
+ border: 1px solid $white-normal;
padding: 5px;
margin: 5px;
max-height: calc(100vh - 100px);
diff --git a/app/assets/stylesheets/pages/icons.scss b/app/assets/stylesheets/pages/icons.scss
deleted file mode 100644
index 407c8db211d..00000000000
--- a/app/assets/stylesheets/pages/icons.scss
+++ /dev/null
@@ -1,12 +0,0 @@
-// CI icon colors
-
-.ci-status-icon {
- &-created {
- fill: $gray-darkest;
- }
-
- &-skipped,
- &-canceled {
- fill: $gl-text-color;
- }
-}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 230b927a17d..4fac0cfb0ba 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -1,3 +1,50 @@
+// Limit MR description for side-by-side diff view
+.limit-container-width {
+ .detail-page-header {
+ max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ .issuable-details {
+ .detail-page-description,
+ .mr-source-target,
+ .mr-state-widget,
+ .merge-manually {
+ max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ .merge-request-tabs-holder {
+ &.affix {
+ border-bottom: 1px solid $border-color;
+
+ .nav-links {
+ border: 0;
+ }
+ }
+
+ .container-fluid {
+ padding-left: 0;
+ padding-right: 0;
+ max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
+ margin-left: auto;
+ margin-right: auto;
+ }
+ }
+ }
+
+ .diffs {
+ .mr-version-controls,
+ .files-changed {
+ max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
+ margin-left: auto;
+ margin-right: auto;
+ }
+ }
+}
+
.issuable-details {
section {
.issuable-discussion {
@@ -7,9 +54,8 @@
// Border around images in issue and MR descriptions.
.description img:not(.emoji) {
- border: 1px solid $table-border-gray;
+ border: 1px solid $white-normal;
padding: 5px;
- margin: 5px;
max-height: calc(100vh - 100px);
}
}
@@ -30,6 +76,7 @@
.color-label {
padding: 6px 10px;
+ border-radius: $label-border-radius;
}
}
@@ -50,7 +97,7 @@
.block {
@include clearfix;
padding: $gl-padding 0;
- border-bottom: 1px solid $border-gray-light;
+ border-bottom: 1px solid $border-gray-normal;
// This prevents the mess when resizing the sidebar
// of elements repositioning themselves..
width: $gutter_inner_width;
@@ -132,7 +179,7 @@
display: none;
}
- .btn-clipboard {
+ .btn-clipboard:hover {
color: $gl-gray;
}
}
@@ -168,7 +215,7 @@
}
.no-value {
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
}
.sidebar-collapsed-icon {
@@ -177,7 +224,7 @@
.gutter-toggle {
margin-top: 7px;
- border-left: 1px solid $border-gray-light;
+ border-left: 1px solid $border-gray-normal;
}
.assignee .avatar {
@@ -215,7 +262,7 @@
}
.participants {
- border-bottom: 1px solid $border-gray-light;
+ border-bottom: 1px solid $border-gray-normal;
}
.hide-collapsed {
@@ -233,7 +280,11 @@
width: 100%;
text-align: center;
padding-bottom: 10px;
- color: #999;
+ color: $issuable-sidebar-color;
+
+ &:hover {
+ color: $gl-gray;
+ }
span {
display: block;
@@ -244,15 +295,17 @@
display: none;
}
+ .avatar:hover {
+ border-color: $issuable-avatar-hover-border;
+ }
+
.btn-clipboard {
border: none;
+ color: $issuable-clipboard-color;
&:hover {
background: transparent;
- }
-
- i {
- color: #999;
+ color: $gl-gray;
}
}
}
@@ -267,20 +320,6 @@
}
}
- .issuable-header-btn {
- background: $gray-normal;
- border: 1px solid $border-gray-normal;
-
- &:hover {
- background: $gray-dark;
- border: 1px solid $border-gray-dark;
- }
-
- &.btn-primary {
- @extend .btn-primary;
- }
- }
-
a {
&:hover {
color: $md-link-color;
@@ -340,7 +379,7 @@
margin-left: 5px;
a {
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
}
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 3e7fc3fa52c..8734a3b1598 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -55,26 +55,45 @@ ul.related-merge-requests > li {
}
.merge-request-status {
- color: $gl-gray;
- font-size: 15px;
- font-weight: bold;
+ font-size: 13px;
+ padding: 0 5px;
+ color: $white-light;
+ height: 20px;
+ border-radius: 3px;
+ line-height: 18px;
+ border: 1px solid;
+
+ &.merged {
+ border-color: darken($blue-normal, 10%);
+ background: $blue-normal;
+ }
+
+ &.closed {
+ border-color: darken($red-normal, 10%);
+ background: $red-normal;
+ }
+
+ &.open {
+ border: 1px solid darken($green-normal, 10%);
+ background: $green-normal;
+ }
}
.merge-request,
.issue {
&.today {
- background: #f3fff2;
- border-color: #e1e8d5;
+ background: $issues-today-bg;
+ border-color: $issues-today-border;
}
&.closed {
background: $gray-light;
- border-color: #e5e5e5;
+ border-color: $border-color;
}
&.merged {
background: $gray-light;
- border-color: #e5e5e5;
+ border-color: $border-color;
}
}
@@ -125,7 +144,7 @@ ul.related-merge-requests > li {
}
.btn {
- background-color: $background-color;
- border: 1px solid $border-gray-light;
+ background-color: $gray-light;
+ border: 1px solid $border-gray-normal;
}
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 397f89f501a..d129eb12a45 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -90,7 +90,7 @@
@media (min-width: $screen-sm-min) {
display: inline-block;
- width: 40%;
+ width: 30%;
margin-left: 10px;
margin-bottom: 0;
vertical-align: middle;
@@ -98,13 +98,14 @@
}
.label {
- padding: 9px;
+ padding: 8px 9px 9px;
font-size: 14px;
}
}
.color-label {
- padding: 3px 4px;
+ padding: 3px 7px;
+ border-radius: $label-border-radius;
}
.dropdown-labels-error {
@@ -198,8 +199,10 @@
}
.label-remove {
- border-left: 1px solid rgba(0, 0, 0, .1);
+ border-left: 1px solid $label-remove-border;
z-index: 3;
+ border-radius: $label-border-radius;
+ padding: 6px 10px 6px 9px;
}
.btn {
@@ -222,6 +225,14 @@
width: 100%;
}
+.label-subscription {
+ vertical-align: middle;
+
+ .dropdown-group-label a {
+ cursor: pointer;
+ }
+}
+
.label-subscribe-button {
.label-subscribe-button-icon {
&[disabled] {
diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss
index 8290519dc25..a7c80dce424 100644
--- a/app/assets/stylesheets/pages/lint.scss
+++ b/app/assets/stylesheets/pages/lint.scss
@@ -1,11 +1,11 @@
.ci-body {
.incorrect-syntax {
- font-size: 19px;
- color: red;
+ font-size: 18px;
+ color: $lint-incorrect-color;
}
.correct-syntax {
- font-size: 19px;
- color: #47a447;
+ font-size: 18px;
+ color: $lint-correct-color;
}
}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 10f67b47998..dd27a06fcd2 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -18,7 +18,7 @@
p {
font-size: 18px;
- color: #888;
+ color: $login-brand-holder-color;
}
h1:first-child {
@@ -174,7 +174,7 @@
.form-control {
&:active,
&:focus {
- background-color: #fff;
+ background-color: $white-light;
}
}
@@ -195,7 +195,7 @@
h2 {
margin-top: 0;
font-size: 14px;
- color: #a00;
+ color: $login-devise-error-color;
}
}
}
@@ -254,27 +254,3 @@
}
}
}
-
-// For sign in pane only, to improve tab order, the following removes the submit button from
-// normal document flow and pins it to the bottom of the form. For context, see !6867 & !6928
-
-.login-box {
- .new_user {
- position: relative;
- padding-bottom: 35px;
-
- @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
- .forgot-password {
- float: none !important;
- margin-top: 5px;
- }
- }
- }
-
- .move-submit-down {
- position: absolute;
- width: 100%;
- bottom: 0;
- }
-}
-
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 756efa9c7fa..36ee5d17211 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -54,6 +54,10 @@
@media (min-width: $screen-sm-min) {
width: 50%;
}
+
+ .dropdown-menu-toggle {
+ width: 100%;
+ }
}
.member-access-text {
@@ -74,6 +78,21 @@
float: right;
}
+ .dropdown {
+ width: 100%;
+ margin-top: 5px;
+
+ .dropdown-menu-toggle {
+ vertical-align: middle;
+ width: 100%;
+ }
+
+ @media (min-width: $screen-sm-min) {
+ margin-top: 0;
+ width: 155px;
+ }
+ }
+
.form-control {
width: 100%;
padding-right: 35px;
@@ -81,12 +100,22 @@
@media (min-width: $screen-sm-min) {
width: 350px;
}
+
+ &.input-short {
+ @media (min-width: $screen-md-min) {
+ width: 170px;
+ }
+
+ @media (min-width: $screen-lg-min) {
+ width: 210px;
+ }
+ }
}
}
.member-search-btn {
position: absolute;
- right: 0;
+ right: 4px;
top: 0;
height: 35px;
padding-left: 10px;
@@ -95,4 +124,8 @@
background: transparent;
border: 0;
outline: 0;
+
+ @media (min-width: $screen-sm-min) {
+ right: 160px;
+ }
}
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index 032feae8854..5a9f199fb34 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -1,3 +1,5 @@
+// Disabled to use the color map for creating color schemes
+// scss-lint:disable ColorVariable
$colors: (
white_header_head_neutral : #e1fad7,
white_line_head_neutral : #effdec,
@@ -98,6 +100,7 @@ $colors: (
solarized_dark_header_not_chosen : rgba(#839496, .25),
solarized_dark_line_not_chosen : rgba(#839496, .15)
);
+// scss-lint:enable ColorVariable
@mixin color-scheme($color) {
@@ -229,14 +232,14 @@ $colors: (
right: 10px;
padding: 0;
outline: none;
- color: #fff;
+ color: $white-light;
width: 75px; // static width to make 2 buttons have same width
height: 19px;
}
}
.btn-success .fa-spinner {
- color: #fff;
+ color: $white-light;
}
.editor-wrap {
@@ -271,7 +274,7 @@ $colors: (
}
.discard-changes-alert {
- background-color: $background-color;
+ background-color: $gray-light;
text-align: right;
padding: $gl-padding-top $gl-padding;
color: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index f8e31a624ec..e779e65eca3 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -3,7 +3,7 @@
*
*/
.mr-state-widget {
- background: $background-color;
+ background: $gray-light;
color: $gl-gray;
border: 1px solid $border-color;
border-radius: 2px;
@@ -24,7 +24,7 @@
.accept_merge_request {
&.ci-pending,
&.ci-running {
- @include btn-orange;
+ @include btn-blue;
}
&.ci-skipped,
@@ -47,6 +47,7 @@
&.right {
float: right;
+ padding-right: 0;
a {
color: $gl-gray;
@@ -60,7 +61,8 @@
}
.ci_widget {
- border-bottom: 1px solid $widget-inner-border;
+ border-bottom: 1px solid $well-inner-border;
+ color: $gl-gray;
svg {
margin-right: 4px;
@@ -69,48 +71,12 @@
overflow: visible;
}
- &.ci-success {
- color: $gl-success;
-
- a.environment,
- a.pipeline {
- color: inherit;
- }
- }
-
&.ci-success_with_warnings {
- color: $gl-success;
i {
color: $gl-warning;
}
}
-
- &.ci-skipped {
- background-color: #eee;
- color: #888;
- }
-
- &.ci-pending {
- color: $gl-warning;
- }
-
- &.ci-running {
- color: $blue-normal;
- }
-
- &.ci-failed,
- &.ci-error {
- color: $gl-danger;
- }
-
- &.ci-canceled {
- color: $gl-gray;
- }
-
- a.monospace {
- color: inherit;
- }
}
.mr-widget-body,
@@ -120,7 +86,7 @@
}
.normal {
- color: #5c5d5e;
+ color: $gl-text-color-dark;
}
.js-deployment-link {
@@ -150,7 +116,7 @@
@media (max-width: $screen-xs-max) {
h4 {
- font-size: 15px;
+ font-size: 14px;
}
p {
@@ -163,6 +129,11 @@
margin-bottom: 4px;
}
+ .btn-grouped {
+ float: none;
+ margin-right: 0;
+ }
+
.accept-action {
width: 100%;
text-align: center;
@@ -177,7 +148,7 @@
}
.mr-widget-footer {
- border-top: 1px solid #eee;
+ border-top: 1px solid $gray-darker;
}
.ci-coverage {
@@ -388,7 +359,7 @@
th {
background-color: $white-light;
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
}
}
}
@@ -404,7 +375,7 @@
}
.mr-version-controls {
- background: $background-color;
+ background: $gray-light;
border-bottom: 1px solid $border-color;
color: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 13402acd8e1..77c523d7310 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -11,6 +11,7 @@
}
.progress {
+ width: 100%;
height: 6px;
}
}
@@ -30,7 +31,6 @@
margin-right: 7px;
}
- // Issue title
span a {
color: $gl-text-color;
word-wrap: break-word;
@@ -39,15 +39,66 @@
}
.milestone-summary {
- margin-bottom: 25px;
-
.milestone-stat {
+ white-space: nowrap;
margin-right: 10px;
+
+ &.with-drilldown {
+ margin-right: 2px;
+ }
}
.remaining-days {
color: $orange-light;
}
+
+ .milestone-stats-and-buttons {
+ display: flex;
+ justify-content: flex-start;
+ flex-wrap: wrap;
+
+ @media (min-width: $screen-xs-min) {
+ justify-content: space-between;
+ flex-wrap: nowrap;
+ }
+ }
+
+ .milestone-progress-buttons {
+ order: 1;
+ margin-top: 10px;
+
+ @media (min-width: $screen-xs-min) {
+ order: 2;
+ margin-top: 0;
+ flex-shrink: 0;
+ }
+
+ .btn {
+ float: left;
+ margin-right: $btn-side-margin;
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+ }
+
+ .milestone-stats {
+ order: 2;
+ width: 100%;
+ padding: 7px 0;
+ flex-shrink: 1;
+
+ @media (min-width: $screen-xs-min) {
+ // when displayed on one line stats go first, buttons second
+ order: 1;
+ }
+ }
+
+ .progress {
+ width: 100%;
+ margin: 15px 0;
+ }
}
.issues-sortable-list,
@@ -57,7 +108,7 @@
margin-top: 7px;
.issuable-number {
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
margin-right: 5px;
}
@@ -72,7 +123,7 @@
padding: 20px 0;
}
-@media (max-width: $screen-sm-min) {
+@media (max-width: $screen-xs-max) {
.milestone-actions {
@include clearfix();
padding-top: $gl-vert-padding;
@@ -82,3 +133,50 @@
}
}
}
+
+.milestone-page-header {
+ display: flex;
+ flex-flow: row;
+ align-items: center;
+ flex-wrap: wrap;
+
+ .status-box {
+ margin-top: 0;
+ }
+
+ .milestone-buttons {
+ margin-left: auto;
+ }
+
+ .status-box {
+ order: 1;
+ }
+
+ .milestone-buttons {
+ order: 2;
+ }
+
+ .header-text-content {
+ order: 3;
+ width: 100%;
+ }
+
+ .milestone-buttons .verbose {
+ display: none;
+ }
+
+ @media (min-width: $screen-xs-min) {
+ .milestone-buttons .verbose {
+ display: inline;
+ }
+
+ .header-text-content {
+ order: 2;
+ width: auto;
+ }
+
+ .milestone-buttons {
+ order: 3;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 16ddef481bd..074abec7692 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -62,7 +62,7 @@
.common-note-form {
.md-area {
padding: $gl-padding-top $gl-padding;
- border: 1px solid $note-form-border-color;
+ border: 1px solid $border-color;
border-radius: $border-radius-base;
transition: border-color ease-in-out 0.15s,
box-shadow ease-in-out 0.15s;
@@ -111,7 +111,7 @@
text-align: center;
font-size: 13px;
- @media (max-width: $screen-md-min) {
+ @media (max-width: $screen-sm-max) {
// On smaller devices the warning becomes the fourth item in the list,
// rather than centering, and grows to span the full width of the
// comment area.
@@ -129,10 +129,10 @@
.note-edit-form {
display: none;
- font-size: 15px;
+ font-size: 14px;
.md-area {
- background-color: #fff;
+ background-color: $white-light;
}
}
@@ -204,7 +204,7 @@
.comment-toolbar {
padding-top: $gl-padding-top;
- color: $note-toolbar-color;
+ color: $gl-gray-light;
border-top: 1px solid $border-color;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 526e9ae5cdd..106c5d4d390 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -3,9 +3,9 @@
*/
@-webkit-keyframes targe3-note {
- from { background: #fffff0; }
- 50% { background: #ffffd3; }
- to { background: #fffff0; }
+ from { background: $note-targe3-outside; }
+ 50% { background: $note-targe3-inside; }
+ to { background: $note-targe3-outside; }
}
ul.notes {
@@ -35,11 +35,114 @@ ul.notes {
.system-note {
font-size: 14px;
- padding-top: 10px;
- padding-bottom: 10px;
- background: #fdfdfd;
+ padding: 0;
+ clear: both;
+
+ &.timeline-entry::after {
+ clear: none;
+ }
+
+ .system-note-message {
+ display: inline-block;
+
+ &::first-letter {
+ text-transform: lowercase;
+ }
+
+ a {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
+
+ p {
+ display: inline-block;
+ margin: 0;
+
+ &::first-letter {
+ text-transform: lowercase;
+ }
+ }
+ }
+
+ .timeline-content {
+ padding: 14px 10px;
+ }
+
+ .note-body {
+ overflow: hidden;
+
+ .system-note-commit-list-toggler {
+ display: none;
+ padding: 10px 0 0;
+ cursor: pointer;
+ position: relative;
+ z-index: 2;
+
+ &:hover {
+ color: $gl-link-color;
+ text-decoration: underline;
+ }
+ }
+
+ .note-text {
+ & p:first-child {
+ display: none;
+ }
+
+ &.system-note-commit-list {
+ max-height: 70px;
+ overflow: hidden;
+ display: block;
+
+ ul {
+ margin: 3px 0 3px 16px !important;
+
+ .gfm-commit {
+ font-family: $monospace_font;
+ font-size: 12px;
+ }
+ }
+
+ p:first-child {
+ display: none;
+ }
+
+ p:last-child {
+ a {
+ color: $gl-text-color;
+
+ &:hover {
+ color: $gl-link-color;
+ }
+ }
+ }
+
+ &::after {
+ content: '';
+ width: 100%;
+ height: 67px;
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ background: linear-gradient(rgba($white-light, 0.1) -100px, $white-light 100%);
+ }
+
+ &.hide-shade {
+ max-height: 100%;
+ overflow: auto;
+
+ &::after {
+ display: none;
+ background: transparent;
+ }
+ }
+ }
+ }
+ }
.timeline-icon {
+ display: none;
+
.avatar {
visibility: hidden;
@@ -63,7 +166,17 @@ ul.notes {
.note {
display: block;
position: relative;
- border-bottom: 1px solid $table-border-gray;
+ border-bottom: 1px solid $white-normal;
+
+ &.note-discussion {
+ &.timeline-entry {
+ padding: 14px 10px;
+ }
+
+ .system-note {
+ padding: 0;
+ }
+ }
&.is-editting {
.note-header,
@@ -88,10 +201,8 @@ ul.notes {
overflow: auto;
word-wrap: break-word;
@include md-typography;
-
// Reset ul style types since we're nested inside a ul already
@include bulleted-list;
-
ul.task-list {
ul:not(.task-list) {
padding-left: 1.3em;
@@ -141,6 +252,22 @@ ul.notes {
}
}
+.page-sidebar-pinned.right-sidebar-expanded {
+ @media (max-width: $screen-md-max) {
+ .note-header {
+ .note-headline-light {
+ display: block;
+ }
+
+ .note-actions {
+ position: absolute;
+ right: 0;
+ top: 0;
+ }
+ }
+ }
+}
+
// Diff code in discussion view
.discussion-body .diff-file {
.file-title {
@@ -164,25 +291,25 @@ ul.notes {
font-family: $regular_font;
td {
- border: 1px solid $table-border-gray;
+ border: 1px solid $white-normal;
border-left: none;
&.notes_line {
vertical-align: middle;
text-align: center;
padding: 10px 0;
- background: $background-color;
+ background: $gray-light;
color: $text-color;
}
&.notes_line2 {
text-align: center;
padding: 10px 0;
- border-left: 1px solid #ddd !important;
+ border-left: 1px solid $note-line2-border !important;
}
&.notes_content {
- background-color: $background-color;
+ background-color: $gray-light;
border-width: 1px 0;
padding: 0;
vertical-align: top;
@@ -222,6 +349,10 @@ ul.notes {
}
}
+.discussion-header {
+ font-size: 14px;
+}
+
.note-headline-light,
.discussion-headline-light {
color: $notes-light-color;
@@ -241,7 +372,7 @@ ul.notes {
.note-actions {
float: right;
margin-left: 10px;
- color: $notes-action-color;
+ color: $gray-darkest;
}
.note-actions {
@@ -252,10 +383,6 @@ ul.notes {
.note-action-button {
margin-left: 10px;
}
-
- @media (min-width: $screen-sm-min) {
- position: relative;
- }
}
.discussion-actions {
@@ -280,9 +407,8 @@ ul.notes {
}
.fa {
- color: $notes-action-color;
+ color: $gray-darkest;
position: relative;
- top: 1px;
font-size: 17px;
}
@@ -318,7 +444,7 @@ ul.notes {
color: $notes-role-color;
font-size: 12px;
line-height: 20px;
- border: 1px solid $notes-role-border-color;
+ border: 1px solid $border-color;
border-radius: $border-radius-base;
}
@@ -340,7 +466,7 @@ ul.notes {
.add-diff-note {
margin-top: -4px;
border-radius: 40px;
- background: #fff;
+ background: $white-light;
padding: 4px;
font-size: 16px;
color: $gl-link-color;
@@ -353,7 +479,7 @@ ul.notes {
&:hover {
background: $gl-info;
- color: #fff;
+ color: $white-light;
@include show-add-diff-note;
}
}
@@ -399,7 +525,7 @@ ul.notes {
.line-resolve-all {
display: inline-block;
padding: 5px 10px;
- background-color: $background-color;
+ background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
@@ -443,10 +569,10 @@ ul.notes {
svg {
position: relative;
- color: $notes-action-color;
+ color: $gray-darkest;
path {
- fill: $notes-action-color;
+ fill: $gray-darkest;
}
}
}
diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss
index 94fbbef3c77..bdf07a99daf 100644
--- a/app/assets/stylesheets/pages/notifications.scss
+++ b/app/assets/stylesheets/pages/notifications.scss
@@ -1,24 +1,16 @@
.notification-list-item {
line-height: 34px;
+
+ .dropdown-menu {
+ @extend .dropdown-menu-align-right;
+ }
}
.notification {
position: relative;
top: 1px;
- > .fa {
+ .fa {
font-size: 18px;
}
}
-
-.ns-part {
- color: $gl-text-green;
-}
-
-.ns-watch {
- color: $gl-success;
-}
-
-.ns-mute {
- color: $gl-danger;
-}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index bf3cb6e7ad9..be22e7bdc79 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -37,12 +37,13 @@
}
}
-.content-list {
-
- &.pipelines,
- &.builds-content-list {
- width: 100%;
- overflow: auto;
+@media (max-width: $screen-md-max) {
+ .content-list {
+ &.pipelines,
+ &.builds-content-list {
+ width: 100%;
+ overflow: auto;
+ }
}
}
@@ -91,14 +92,6 @@
}
}
- .ci-status {
-
- svg {
- top: 1px;
- margin-right: 0;
- }
- }
-
a:hover {
text-decoration: none;
}
@@ -109,10 +102,6 @@
float: none;
}
- .api {
- color: $code-color;
- }
-
.branch-commit {
.branch-name {
@@ -129,7 +118,7 @@
height: 14px;
width: 14px;
vertical-align: middle;
- fill: $table-text-gray;
+ fill: $gl-gray-light;
}
.fa {
@@ -199,7 +188,7 @@
width: 8px;
position: absolute;
right: -7px;
- bottom: 8px;
+ bottom: 9px;
border-bottom: 2px solid $border-color;
}
}
@@ -212,7 +201,7 @@
.duration,
.finished-at {
- color: $table-text-gray;
+ color: $gl-gray-light;
margin: 4px 0;
.fa {
@@ -233,7 +222,7 @@
.btn {
margin: 0;
- color: $table-text-gray;
+ color: $gl-gray-light;
}
.cancel-retry-btns {
@@ -246,10 +235,10 @@
.dropdown-toggle,
.dropdown-menu {
- color: $table-text-gray;
+ color: $gl-gray-light;
.fa {
- color: $table-text-gray;
+ color: $gl-gray-light;
font-size: 14px;
}
@@ -292,68 +281,169 @@
}
}
+.admin-builds-table {
+ .ci-table td:last-child {
+ min-width: 120px;
+ }
+}
+
// Pipeline visualization
+.pipeline-actions {
+ border-bottom: none;
+}
-.toggle-pipeline-btn {
- background-color: $gray-dark;
+.tab-pane {
+ &.pipelines {
+ .ci-table {
+ min-width: 900px;
+ }
- &.graph-collapsed {
- background-color: $white-light;
+ .content-list.pipelines {
+ overflow: auto;
+ }
+
+ .stage {
+ max-width: 100px;
+ width: 100px;
+ }
+
+ .pipeline-actions {
+ min-width: initial;
+ }
+ }
+
+ &.builds {
+ .ci-table {
+ tr {
+ height: 71px;
+ }
+ }
}
}
+// Pipeline graph
.pipeline-graph {
width: 100%;
+ background-color: $gray-light;
+ padding: $gl-padding;
overflow: auto;
white-space: nowrap;
transition: max-height 0.3s, padding 0.3s;
- &.graph-collapsed {
- max-height: 0;
- padding: 0 16px;
- }
-}
-
-.pipeline-visualization {
- position: relative;
-
ul {
padding: 0;
}
-}
-.stage-column {
- display: inline-block;
- vertical-align: top;
+ a {
+ text-decoration: none;
+ color: $gl-text-color-light;
+ }
- &:not(:last-child) {
- margin-right: 44px;
+ svg {
+ vertical-align: middle;
+ margin-right: 3px;
}
- &.left-margin {
- &:not(:first-child) {
- margin-left: 44px;
+ .stage-column {
+ display: inline-block;
+ vertical-align: top;
- .left-connector {
- &::before {
- content: '';
- position: absolute;
- top: 48%;
- left: -48px;
- border-top: 2px solid $border-color;
- width: 48px;
- height: 1px;
+ &:not(:last-child) {
+ margin-right: 44px;
+ }
+
+ &.left-margin {
+ &:not(:first-child) {
+ margin-left: 44px;
+
+ .left-connector {
+ &::before {
+ content: '';
+ position: absolute;
+ top: 48%;
+ left: -48px;
+ border-top: 2px solid $border-color;
+ width: 48px;
+ height: 1px;
+ }
}
}
}
- }
- &.no-margin {
- margin: 0;
- }
+ &.no-margin {
+ margin: 0;
+ }
+
+ li {
+ list-style: none;
+ }
- li {
- list-style: none;
+ &:last-child {
+ .build {
+ // Remove right connecting horizontal line from first build in last stage
+ &:first-child {
+ &::after {
+ border: none;
+ }
+ }
+ // Remove right curved connectors from all builds in last stage
+ &:not(:first-child) {
+ &::after {
+ border: none;
+ }
+ }
+ // Remove opposite curve
+ .curve {
+ &::before {
+ display: none;
+ }
+ }
+ }
+ }
+
+ &:first-child {
+ .build {
+ // Remove left curved connectors from all builds in first stage
+ &:not(:first-child) {
+ &::before {
+ border: none;
+ }
+ }
+ // Remove opposite curve
+ .curve {
+ &::after {
+ display: none;
+ }
+ }
+ }
+ }
+
+ // Curve first child connecting lines in opposite direction
+ .curve {
+ display: none;
+
+ &::before,
+ &::after {
+ content: '';
+ width: 21px;
+ height: 25px;
+ position: absolute;
+ top: -32px;
+ border-top: 2px solid $border-color;
+ }
+
+ &::after {
+ left: -44px;
+ border-right: 2px solid $border-color;
+ border-radius: 0 20px;
+ }
+
+ &::before {
+ right: -44px;
+ border-left: 2px solid $border-color;
+ border-radius: 20px 0 0;
+ }
+ }
}
.stage-name {
@@ -367,174 +457,78 @@
.build {
border: 1px solid $border-color;
- position: relative;
- padding: 7px 10px 8px;
border-radius: 30px;
+ background-color: $white-light;
+ position: relative;
+ padding: 8px 4px 9px 10px;
width: 186px;
margin-bottom: 10px;
+ white-space: normal;
&:hover {
- background-color: $gray-lighter;
- }
-
- &.playable {
-
- svg {
- height: 13px;
- width: 20px;
- position: relative;
- top: 1px;
+ background-color: $stage-hover-bg;
+ border: 1px solid $stage-hover-border;
- path {
- fill: $layout-link-gray;
- }
+ a,
+ .dropdown-counter-badge,
+ .dropdown-menu-toggle {
+ color: $gl-text-color;
}
- }
- .build-content {
- display: -ms-flexbox;
- display: -webkit-flex;
- display: flex;
- width: 164px;
+ .grouped-pipeline-dropdown a {
+ color: $gl-text-color-light;
- .ci-status-icon {
- svg {
- height: 20px;
- width: 20px;
+ &:hover {
+ color: $gl-text-color;
}
}
+ }
- .tooltip {
- white-space: nowrap;
+ .ci-status-icon {
+ position: relative;
+ top: 1px;
+ }
- .tooltip-inner {
- overflow: hidden;
- text-overflow: ellipsis;
- }
- }
+ .ci-status-icon svg {
+ height: 20px;
+ width: 20px;
+ }
- .ci-status-text {
- width: 135px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- vertical-align: middle;
+ .arrow {
+ &::before,
+ &::after {
+ content: '';
display: inline-block;
- position: relative;
- top: -1px;
- }
-
- a {
- color: $gl-text-color-light;
- text-decoration: none;
- }
-
- .dropdown-menu-toggle {
- background-color: transparent;
- border: none;
- width: auto;
- padding: 0;
- color: $gl-text-color-light;
- flex-grow: 1;
-
- .ci-status-text {
- max-width: 112px;
- width: auto;
- }
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+ top: 18px;
}
- .grouped-pipeline-dropdown {
- padding: 0;
- width: 186px;
- left: auto;
- right: -197px;
- top: -9px;
-
- ul {
- max-height: 245px;
- overflow: auto;
-
- li:first-child {
- padding-top: 8px;
- }
-
- li:last-child {
- padding-bottom: 8px;
- }
- }
-
- a {
- color: $gl-text-color;
- padding: 7px 8px 8px;
-
- &:hover {
- background-color: $blue-light-transparent;
- border-radius: 3px;
-
- .ci-status-text {
- text-decoration: none;
- }
- }
- }
-
- svg {
- width: 14px;
- height: 14px;
- }
-
- .ci-status-text {
- width: 112px;
- }
-
- .arrow {
- &::before,
- &::after {
- content: '';
- display: inline-block;
- position: absolute;
- width: 0;
- height: 0;
- border-color: transparent;
- border-style: solid;
- top: 18px;
- }
-
- &::before {
- left: -5px;
- margin-top: -6px;
- border-width: 7px 5px 7px 0;
- border-right-color: $border-color;
- }
-
- &::after {
- left: -4px;
- margin-top: -9px;
- border-width: 10px 7px 10px 0;
- border-right-color: $white-light;
- }
- }
+ &::before {
+ left: -5px;
+ margin-top: -6px;
+ border-width: 7px 5px 7px 0;
+ border-right-color: $border-color;
}
- .badge {
- background-color: $gray-darker;
- color: $gl-text-color-light;
- font-weight: normal;
- margin-left: $btn-xs-side-margin;
+ &::after {
+ left: -4px;
+ margin-top: -9px;
+ border-width: 10px 7px 10px 0;
+ border-right-color: $white-light;
}
}
- svg {
- vertical-align: middle;
- margin-right: 5px;
- }
-
// Connect first build in each stage with right horizontal line
&:first-child {
&::after {
content: '';
position: absolute;
top: 48%;
- right: -48px;
+ right: -49px;
border-top: 2px solid $border-color;
width: 48px;
height: 1px;
@@ -582,120 +576,161 @@
}
}
- &:last-child {
- .build {
- // Remove right connecting horizontal line from first build in last stage
- &:first-child {
- &::after {
- border: none;
- }
- }
- // Remove right curved connectors from all builds in last stage
- &:not(:first-child) {
- &::after {
- border: none;
- }
- }
- // Remove opposite curve
- .curve {
- &::before {
- display: none;
- }
- }
- }
- }
-
- &:first-child {
- .build {
- // Remove left curved connectors from all builds in first stage
- &:not(:first-child) {
- &::before {
- border: none;
- }
- }
- // Remove opposite curve
- .curve {
- &::after {
- display: none;
- }
- }
- }
+ .ci-status-text {
+ max-width: 110px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: bottom;
+ display: inline-block;
+ position: relative;
+ font-weight: 100;
}
- // Curve first child connecting lines in opposite direction
- .curve {
- display: none;
+ .dropdown-menu-toggle {
+ background-color: transparent;
+ border: none;
+ padding: 0;
+ color: $gl-text-color-light;
+ white-space: normal;
+ overflow: visible;
- &::before,
- &::after {
- content: '';
- width: 21px;
- height: 25px;
- position: absolute;
- top: -32px;
- border-top: 2px solid $border-color;
+ &:focus {
+ outline: none;
}
- &::after {
- left: -44px;
- border-right: 2px solid $border-color;
- border-radius: 0 20px;
- }
+ &:hover {
+ color: $gl-text-color;
- &::before {
- right: -44px;
- border-left: 2px solid $border-color;
- border-radius: 20px 0 0;
+ .dropdown-counter-badge {
+ color: $gl-text-color;
+ }
}
}
-}
-.pipeline-actions {
- border-bottom: none;
-}
-
-.toggle-pipeline-btn {
-
- .fa {
- color: $dropdown-header-color;
+ .dropdown-counter-badge {
+ float: right;
+ clear: right;
+ color: $border-color;
+ font-weight: 100;
+ font-size: 15px;
+ margin-right: 2px;
}
-}
-.tab-pane {
+ .grouped-pipeline-dropdown {
+ padding: 0;
+ width: 191px;
+ left: auto;
+ right: -206px;
+ top: -11px;
+ box-shadow: 0 1px 5px $black-transparent;
- &.pipelines {
+ a {
+ display: inline-block;
- .ci-table {
- min-width: 900px;
+ &:hover {
+ background-color: $stage-hover-bg;
+ }
}
- .content-list.pipelines {
+ ul {
+ max-height: 245px;
overflow: auto;
- }
+ margin: 5px 0;
- .stage {
- max-width: 100px;
- width: 100px;
+ li {
+ margin: 0 5px;
+ padding-left: 0;
+ padding-bottom: 0;
+ margin-bottom: 0;
+ line-height: 1.2;
+ }
}
- .pipeline-actions {
- min-width: initial;
- }
- }
+ .dropdown-build {
+ color: $gl-text-color-light;
- &.builds {
+ a.ci-action-icon-container {
+ padding: 0;
+ font-size: 11px;
+ float: right;
+ margin-top: 4px;
+ display: inline-block;
+ position: relative;
- .ci-table {
- tr {
- height: 71px;
+ i {
+ font-size: 11px;
+ margin-top: 0;
+ }
+ }
+
+ &:hover {
+ background-color: $stage-hover-bg;
+ border-radius: 3px;
+ color: $gl-text-color;
+ }
+
+ .ci-action-icon-container {
+ i {
+ width: 25px;
+ height: 25px;
+
+ &::before {
+ top: 1px;
+ left: 1px;
+ }
+ }
+ }
+
+ .stage {
+ max-width: 100px;
+ width: 100px;
+ }
+
+ .ci-status-icon svg {
+ height: 18px;
+ width: 18px;
+ }
+
+ .ci-status-text {
+ max-width: 95px;
+ padding-bottom: 3px;
+ position: relative;
+ top: 3px;
}
}
}
}
-.ci-status-icon-created {
+// Action Icons
+.ci-action-icon-container .ci-action-icon-wrapper {
+ float: right;
+ margin-top: -4px;
- svg {
- fill: $gray-darkest;
+ 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;
+ }
+
+ &:hover {
+ color: $gl-text-color;
+ background-color: $stage-hover-bg;
+ border: 1px solid $stage-hover-bg;
+ }
+ }
+
+ .ci-play-icon {
+ padding: 5px 5px 5px 7px;
}
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 6fab97a71aa..8b1976bd925 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -60,8 +60,8 @@
.account-well {
padding: 10px;
- background-color: $help-well-bg;
- border: 1px solid $help-well-border;
+ background-color: $gray-light;
+ border: 1px solid $border-color;
border-radius: $border-radius-base;
ul {
@@ -136,7 +136,7 @@
.provider-btn-group {
display: inline-block;
margin-right: 10px;
- border: 1px solid $provider-btn-group-border;
+ border: 1px solid $border-color;
border-radius: 3px;
&:last-child {
@@ -147,7 +147,7 @@
.provider-btn-image {
display: inline-block;
padding: 5px 10px;
- border-right: 1px solid $provider-btn-group-border;
+ border-right: 1px solid $border-color;
> img {
width: 20px;
@@ -180,7 +180,7 @@
.modal-dialog {
width: 380px;
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
width: auto;
}
@@ -198,7 +198,7 @@
}
.personal-access-tokens-never-expires-label {
- color: $personal-access-tokens-disabled-label-color;
+ color: $note-disabled-comment-color;
}
.datepicker.personal-access-tokens-expires-at .ui-state-disabled span {
@@ -261,4 +261,14 @@ table.u2f-registrations {
td:not(:last-child) {
border-right: solid 1px transparent;
}
+}
+
+.oauth-application-show {
+ .scope-name {
+ font-weight: 600;
+ }
+
+ .scopes-list {
+ padding-left: 18px;
+ }
} \ No newline at end of file
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index f7d54564530..3b1b375133d 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -6,12 +6,6 @@
}
}
-.no-ssh-key-message,
-.project-limit-message {
- background-color: #f28d35;
- margin-bottom: 0;
-}
-
.new_project,
.edit-project {
@@ -76,7 +70,7 @@
&.static-namespace {
height: 35px;
border-radius: 3px;
- border: 1px solid #e5e5e5;
+ border: 1px solid $border-color;
}
&+ .select2 a {
@@ -86,7 +80,8 @@
}
}
-.project-home-panel {
+.project-home-panel,
+.group-home-panel {
padding-top: 24px;
padding-bottom: 24px;
@@ -94,7 +89,8 @@
border-bottom: 1px solid $border-color;
}
- .project-avatar {
+ .project-avatar,
+ .group-avatar {
float: none;
margin: 0 auto;
border: none;
@@ -104,7 +100,8 @@
}
}
- .project-title {
+ .project-title,
+ .group-title {
margin-top: 10px;
margin-bottom: 10px;
font-size: 24px;
@@ -118,10 +115,11 @@
}
}
- .project-home-desc {
+ .project-home-desc,
+ .group-home-desc {
margin-left: auto;
margin-right: auto;
- margin-bottom: 15px;
+ margin-bottom: 0;
max-width: 700px;
> p {
@@ -141,13 +139,22 @@
}
}
-.project-repo-buttons {
- font-size: 0;
+.nav > .project-repo-buttons {
+ margin-top: 0;
+}
+
+.project-repo-buttons,
+.group-buttons {
+ margin-top: 15px;
.btn {
@include btn-gray;
padding: 3px 10px;
+ &:last-child {
+ margin-left: 0;
+ }
+
.fa {
color: $layout-link-gray;
}
@@ -168,12 +175,23 @@
}
}
- .project-repo-btn-group,
+ .download-button,
+ .dropdown-toggle,
.notification-dropdown,
.project-dropdown {
margin-left: 10px;
}
+ .notification-dropdown .dropdown-menu {
+ @extend .dropdown-menu-align-right;
+ }
+
+ .download-button {
+ @media (max-width: $screen-md-max) {
+ margin-left: 0;
+ }
+ }
+
.count-buttons {
display: inline-block;
vertical-align: top;
@@ -205,7 +223,7 @@
left: 0;
margin-top: -6px;
border-width: 7px 5px 7px 0;
- border-right-color: #dce0e5;
+ border-right-color: $count-arrow-border;
pointer-events: none;
}
@@ -220,7 +238,7 @@
left: 1px;
margin-top: -9px;
border-width: 10px 7px 10px 0;
- border-right-color: #fff;
+ border-right-color: $white-light;
pointer-events: none;
}
}
@@ -228,7 +246,7 @@
.count {
@include btn-gray;
display: inline-block;
- background: white;
+ background: $white-light;
border-radius: 2px;
border-width: 1px;
border-style: solid;
@@ -250,7 +268,7 @@
}
&:hover {
- background: #fff;
+ background: $white-light;
}
}
}
@@ -282,7 +300,7 @@
.option-descr {
margin-left: 29px;
- color: #54565b;
+ color: $project-option-descr-color;
}
}
}
@@ -290,7 +308,7 @@
.save-project-loader {
margin-top: 50px;
margin-bottom: 50px;
- color: #555;
+ color: $save-project-loader-color;
}
.transfer-project .select2-container {
@@ -353,7 +371,7 @@ a.deploy-project-label {
> li + li::before {
padding: 0 3px;
- color: #999;
+ color: $project-breadcrumb-color;
}
a {
@@ -396,13 +414,13 @@ a.deploy-project-label {
width: 100px;
height: 100px;
background-color: $gray-light;
- border: 1px solid $gray-dark;
+ border: 1px solid $white-normal;
margin: 0 auto;
border-radius: 50%;
i {
font-size: 100px;
- color: $gray-dark;
+ color: $white-normal;
}
}
@@ -458,6 +476,20 @@ a.deploy-project-label {
}
}
+.page-sidebar-pinned {
+ .project-stats .nav > li.right {
+ @media (min-width: $screen-lg-min) {
+ float: none;
+ }
+ }
+
+ .download-button {
+ @media (min-width: $screen-lg-min) {
+ margin-left: 0;
+ }
+ }
+}
+
.project-stats {
font-size: 0;
border-bottom: 1px solid $border-color;
@@ -474,12 +506,12 @@ a.deploy-project-label {
margin-right: $gl-padding;
}
- &.project-repo-buttons-right {
- margin-top: 10px;
+ &.right {
+ vertical-align: top;
+ margin-top: 0;
- @media (min-width: $screen-md-min) {
+ @media (min-width: $screen-lg-min) {
float: right;
- margin-top: 0;
}
}
}
@@ -487,7 +519,7 @@ a.deploy-project-label {
.nav > li > a {
padding: 0;
background-color: transparent;
- font-size: 15px;
+ font-size: 14px;
line-height: 29px;
color: $notes-light-color;
@@ -498,7 +530,7 @@ a.deploy-project-label {
}
li.missing {
- border: 1px dashed $border-gray-light;
+ border: 1px dashed $border-gray-normal;
border-radius: $border-radius-default;
a {
@@ -515,20 +547,20 @@ a.deploy-project-label {
}
pre.light-well {
- border-color: #f1f1f1;
+ border-color: $well-light-border;
}
.git-empty {
margin: 0 7px 7px;
h5 {
- color: #5c5d5e;
+ color: $gl-text-color-dark;
}
.light-well {
border-radius: 2px;
- color: #5b6169;
+ color: $well-light-text-color;
font-size: 13px;
line-height: 1.6em;
}
@@ -553,7 +585,7 @@ pre.light-well {
@include basic-list;
.project-row {
- border-color: $table-border-color;
+ border-color: $white-normal;
.project-full-name {
@include str-truncated;
@@ -605,7 +637,7 @@ pre.light-well {
&.container-fluid {
padding-top: 12px;
padding-bottom: 12px;
- background-color: $background-color;
+ background-color: $gray-light;
border: 1px solid $border-color;
border-right-width: 0;
border-left-width: 0;
@@ -682,7 +714,7 @@ pre.light-well {
.form-control {
@extend .monospace;
- background: #fff;
+ background: $white-light;
font-size: 14px;
margin-left: -1px;
cursor: auto;
@@ -692,17 +724,17 @@ pre.light-well {
.cannot-be-merged,
.cannot-be-merged:hover {
- color: #e62958;
+ color: $error-exclamation-point;
margin-top: 2px;
}
.private-forks-notice .private-fork-icon {
i:nth-child(1) {
- color: #2aa056;
+ color: $project-private-forks-notice-odd;
}
i:nth-child(2) {
- color: #fff;
+ color: $white-light;
}
}
@@ -842,3 +874,11 @@ pre.light-well {
pointer-events: none;
}
}
+
+.variables-table {
+ table-layout: fixed;
+
+ .variable-key {
+ width: 30%;
+ }
+}
diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss
index 7b3878c91df..9b6ff237557 100644
--- a/app/assets/stylesheets/pages/runners.scss
+++ b/app/assets/stylesheets/pages/runners.scss
@@ -1,27 +1,27 @@
.runner-state {
padding: 6px 12px;
margin-right: 10px;
- color: #fff;
+ color: $white-light;
&.runner-state-shared {
- background: #32b186;
+ background: $runner-state-shared-bg;
}
&.runner-state-specific {
- background: #3498db;
+ background: $runner-state-specific-bg;
}
}
.runner-status-online {
- color: green;
+ color: $runner-status-online-color;
}
.runner-status-offline {
- color: gray;
+ color: $runner-status-offline-color;
}
.runner-status-paused {
- color: red;
+ color: $runner-status-paused-color;
}
.runner {
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index bf688af50e2..cedd4cb2987 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -8,6 +8,10 @@
border-bottom: none;
}
}
+
+ .blob-result {
+ margin: 5px 0;
+ }
}
.search {
@@ -21,6 +25,11 @@
padding: 4px;
width: $search-input-width;
line-height: 24px;
+
+ &:hover {
+ border-color: lighten($dropdown-input-focus-border, 20%);
+ box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
+ }
}
.location-text {
@@ -28,11 +37,9 @@
}
.search-input {
- padding-right: 20px;
border: none;
font-size: 14px;
- outline: none;
- padding: 0;
+ padding: 0 20px 0 0;
margin-left: 5px;
line-height: 25px;
width: 98%;
@@ -44,9 +51,9 @@
border-radius: $border-radius-default;
font-size: 14px;
font-style: normal;
- color: $location-badge-color;
+ color: $note-disabled-comment-color;
display: inline-block;
- background-color: $location-badge-bg;
+ background-color: $gray-normal;
vertical-align: top;
cursor: default;
}
@@ -133,7 +140,7 @@
.search-input-wrap {
i {
- color: $location-icon-active-color;
+ color: $layout-link-gray;
}
}
}
@@ -229,6 +236,5 @@
&:hover,
&:focus {
color: $gl-link-color;
- outline: none;
}
}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 2e8f356298d..ddee2c95247 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -1,5 +1,5 @@
.settings-list-icon {
- color: $gl-placeholder-color;
+ color: $gl-gray-light;
font-size: $settings-icon-size;
line-height: 42px;
}
@@ -20,3 +20,7 @@
.danger-title {
color: $gl-danger;
}
+
+.service-settings .control-label {
+ padding-top: 0;
+}
diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss
deleted file mode 100644
index 857eb76131a..00000000000
--- a/app/assets/stylesheets/pages/snippets.scss
+++ /dev/null
@@ -1,42 +0,0 @@
-.snippet-form-holder .file-holder .file-title {
- padding: 2px;
-}
-
-.markdown-snippet-copy {
- position: fixed;
- top: -10px;
- left: -10px;
- max-height: 0;
- max-width: 0;
-}
-
-.snippet-file-content {
- border-radius: 3px;
- margin-bottom: $gl-padding;
-
- .btn-clipboard {
- @extend .btn;
- }
-}
-
-.project-snippets .awards {
- border-bottom: 1px solid $table-border-color;
- padding-bottom: $gl-padding;
-}
-
-.snippet-title {
- font-size: 24px;
- font-weight: 600;
- padding: $gl-padding;
- padding-left: 0;
-}
-
-.snippet-actions {
- @media (min-width: $screen-sm-min) {
- float: right;
- }
-}
-
-.snippet-scope-menu .btn-new {
- margin-top: 15px;
-}
diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss
index 69288b31cc4..dfa4d033fb8 100644
--- a/app/assets/stylesheets/pages/stat_graph.scss
+++ b/app/assets/stylesheets/pages/stat_graph.scss
@@ -1,16 +1,16 @@
.tint-box {
- background: #f3f3f3;
+ background: $stat-graph-common-bg;
position: relative;
margin-bottom: 10px;
}
.area {
- fill: #1db34f;
+ fill: $stat-graph-area-fill;
fill-opacity: 0.5;
}
.axis {
- fill: #aaa;
+ fill: $stat-graph-axis-fill;
font-size: 10px;
}
@@ -37,26 +37,26 @@
@include make-md-column(6);
margin-top: 10px;
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
width: 100%;
}
}
.person .spark {
display: block;
- background: #f3f3f3;
+ background: $stat-graph-common-bg;
width: 100%;
}
.person .area-contributor {
- fill: #f17f49;
+ fill: $stat-graph-orange-fill;
}
}
.selection rect {
- fill: #333;
+ fill: $stat-graph-selection-fill;
fill-opacity: 0.1;
- stroke: #333;
+ stroke: $stat-graph-selection-stroke;
stroke-width: 1px;
stroke-opacity: 0.4;
shape-rendering: crispedges;
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 92997eae8b9..f3b0608e545 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -2,7 +2,7 @@
.ci-status {
padding: 2px 7px;
margin-right: 10px;
- border: 1px solid #eee;
+ border: 1px solid $gray-darker;
white-space: nowrap;
border-radius: 4px;
@@ -11,80 +11,108 @@
text-decoration: none;
}
+ svg {
+ height: 13px;
+ width: 13px;
+ position: relative;
+ top: 1px;
+ margin-right: 3px;
+ overflow: visible;
+ }
+
&.ci-failed {
color: $gl-danger;
border-color: $gl-danger;
+
+ &:not(span):hover {
+ background-color: rgba( $gl-danger, .07);
+ }
+
+ svg {
+ fill: $gl-danger;
+ }
}
&.ci-success,
&.ci-success_with_warnings {
color: $gl-success;
border-color: $gl-success;
+
+ &:not(span):hover {
+ background-color: rgba( $gl-success, .07);
+ }
+
+ svg {
+ fill: $gl-success;
+ }
}
&.ci-info {
color: $gl-info;
border-color: $gl-info;
+
+ &:not(span):hover {
+ background-color: rgba( $gl-info, .07);
+ }
+
+ svg {
+ fill: $gl-info;
+ }
}
&.ci-canceled,
- &.ci-skipped,
&.ci-disabled {
color: $gl-gray;
border-color: $gl-gray;
+
+ &:not(span):hover {
+ background-color: rgba( $gl-gray, .07);
+ }
+
+ svg {
+ fill: $gl-gray;
+ }
}
&.ci-pending {
color: $gl-warning;
border-color: $gl-warning;
+
+ &:not(span):hover {
+ background-color: rgba( $gl-warning, .07);
+ }
+
+ svg {
+ fill: $gl-warning;
+ }
}
&.ci-running {
color: $blue-normal;
border-color: $blue-normal;
- }
- &.ci-created {
- color: $table-text-gray;
- border-color: $table-text-gray;
+ &:not(span):hover {
+ background-color: rgba( $blue-normal, .07);
+ }
svg {
- fill: $table-text-gray;
+ fill: $blue-normal;
}
}
- svg {
- height: 13px;
- width: 13px;
- position: relative;
- top: 1px;
- margin: 0 3px;
- overflow: visible;
- }
- }
-
- .ci-status-icon-success {
- color: $gl-success;
- }
+ &.ci-created,
+ &.ci-skipped {
+ color: $gl-gray-light;
+ border-color: $gl-gray-light;
- .ci-status-icon-failed {
- color: $gl-danger;
- }
-
- .ci-status-icon-pending,
- .ci-status-icon-success_with_warning {
- color: $gl-warning;
- }
-
- .ci-status-icon-running {
- color: $blue-normal;
- }
+ &:not(span):hover {
+ background-color: rgba( $gl-gray-light, .07);
+ }
- .ci-status-icon-canceled,
- .ci-status-icon-disabled,
- .ci-status-icon-not-found,
- .ci-status-icon-skipped {
- color: $gl-gray;
+ svg {
+ fill: $gl-gray-light;
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/pages/tags.scss b/app/assets/stylesheets/pages/tags.scss
deleted file mode 100644
index 24ebd3e7cfa..00000000000
--- a/app/assets/stylesheets/pages/tags.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-.tag-buttons {
- line-height: 40px;
-
- .btn:not(.dropdown-toggle) {
- margin-left: 10px;
- }
-}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index b3aef2fdd32..508b30f3947 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -11,7 +11,7 @@
background: $todo-alert-blue;
margin-left: -17px;
font-size: 11px;
- color: white;
+ color: $white-light;
padding: 3px;
padding-top: 1px;
padding-bottom: 1px;
@@ -81,7 +81,7 @@
word-wrap: break-word;
.md {
- color: #7f8fa4;
+ color: $gl-grayish-blue;
font-size: $gl-font-size;
.label {
@@ -90,7 +90,7 @@
}
p {
- color: #5c5d5e;
+ color: $gl-text-color-dark;
}
}
@@ -102,7 +102,7 @@
border: none;
background: $gray-light;
border-radius: 0;
- color: #777;
+ color: $todo-body-pre-color;
margin: 0 20px;
overflow: hidden;
}
@@ -146,7 +146,7 @@
.todo-body {
margin: 0;
- border-left: 2px solid #ddd;
+ border-left: 2px solid $todo-body-border;
padding-left: 10px;
}
}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 2b836fa1f4a..c0341db7289 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -20,8 +20,8 @@
margin-bottom: 0;
tr {
- border-bottom: 1px solid $table-border-gray;
- border-top: 1px solid $table-border-gray;
+ border-bottom: 1px solid $white-normal;
+ border-top: 1px solid $white-normal;
td,
th {
@@ -31,7 +31,7 @@
.last-commit {
@include str-truncated(506px);
- @media (min-width: $screen-sm-max) and (max-width: $screen-md-max) {
+ @media (min-width: $screen-md-min) and (max-width: $screen-md-max) {
@include str-truncated(450px);
}
@@ -39,7 +39,7 @@
.commit-history-link-spacer {
margin: 0 10px;
- color: $table-border-color;
+ color: $white-normal;
}
&:hover {
@@ -53,7 +53,7 @@
&.selected {
td {
- background: $gray-dark;
+ background: $white-normal;
border-top: 1px solid $border-gray-dark;
border-bottom: 1px solid $border-gray-dark;
}
@@ -134,7 +134,7 @@
.blob-commit-info {
list-style: none;
padding: $gl-padding;
- background: $background-color;
+ background: $gray-light;
border: 1px solid $border-color;
border-bottom: none;
margin: 0;
diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss
index e73cecc92be..8c87bc3cafd 100644
--- a/app/assets/stylesheets/pages/ui_dev_kit.scss
+++ b/app/assets/stylesheets/pages/ui_dev_kit.scss
@@ -7,11 +7,11 @@
.example {
&::before {
content: "Example";
- color: #bbb;
+ color: $ui-dev-kit-example-color;
}
padding: 15px;
- border: 1px dashed #ddd;
+ border: 1px dashed $ui-dev-kit-example-border;
margin-bottom: 15px;
}
}
diff --git a/app/assets/stylesheets/pages/votes.scss b/app/assets/stylesheets/pages/votes.scss
deleted file mode 100644
index dc9a7d71e8b..00000000000
--- a/app/assets/stylesheets/pages/votes.scss
+++ /dev/null
@@ -1,4 +0,0 @@
-.votes-inline {
- display: inline-block;
- margin: 0 8px;
-}
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index dfaeba41cf6..b9f81533150 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -4,3 +4,128 @@
margin-right: auto;
padding-right: 7px;
}
+
+.wiki-page-header {
+ @extend .top-area;
+ position: relative;
+
+ .wiki-page-title {
+ margin: 0;
+ font-size: 22px;
+ }
+
+ .wiki-last-edit-by {
+ color: $gl-gray-light;
+
+ strong {
+ color: $gl-text-color;
+ }
+ }
+
+ .light {
+ font-weight: normal;
+ color: $gl-gray-light;
+ }
+
+ .git-access-header {
+ padding: 16px 40px 11px 0;
+ line-height: 28px;
+ font-size: 18px;
+ }
+
+ .git-clone-holder {
+ width: 100%;
+ padding-bottom: 40px;
+ }
+
+ button.sidebar-toggle {
+ position: absolute;
+ right: 0;
+ top: 11px;
+ display: block;
+ }
+
+ @media (min-width: $screen-sm-min) {
+ &.has-sidebar-toggle {
+ padding-right: 40px;
+ }
+
+ .git-clone-holder {
+ width: 480px;
+ }
+
+ .nav-controls {
+ width: auto;
+ min-width: 50%;
+ white-space: nowrap;
+ }
+ }
+
+ @media (min-width: $screen-md-min) {
+ &.has-sidebar-toggle {
+ padding-right: 0;
+ }
+
+ button.sidebar-toggle {
+ display: none;
+ }
+ }
+}
+
+.wiki-git-access {
+ margin: $gl-padding 0;
+
+ h3 {
+ font-size: 22px;
+ font-weight: normal;
+ margin-top: 1.4em;
+ }
+}
+
+.right-sidebar.wiki-sidebar {
+ padding: $gl-padding 0;
+
+ &.right-sidebar-collapsed {
+ display: none;
+ }
+
+ .blocks-container {
+ padding: 0 $gl-padding;
+ }
+
+ .block {
+ width: 100%;
+ }
+
+ a {
+ color: $layout-link-gray;
+
+ &:hover,
+ &.active {
+ color: $black;
+ }
+ }
+
+ .active > a {
+ color: $black;
+ }
+
+ ul.wiki-pages,
+ ul.wiki-pages li {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ }
+
+ ul.wiki-pages li {
+ margin: 5px 0 10px;
+ }
+
+ .wiki-sidebar-header {
+ padding: 0 $gl-padding $gl-padding;
+
+ .gutter-toggle {
+ margin-top: 0;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss
index 3fa7fa3d7e3..b085c56390d 100644
--- a/app/assets/stylesheets/pages/xterm.scss
+++ b/app/assets/stylesheets/pages/xterm.scss
@@ -18,7 +18,267 @@
$l-blue: #81a2be;
$l-magenta: #b294bb;
$l-cyan: #8abeb7;
- $l-white: $ci-text-color;
+ $l-white: $gray-darkest;
+
+ /*
+ * xterm colors
+ */
+ $xterm-fg-0: $black;
+ $xterm-fg-1: #800000;
+ $xterm-fg-2: #008000;
+ $xterm-fg-3: #808000;
+ $xterm-fg-4: #000080;
+ $xterm-fg-5: #800080;
+ $xterm-fg-6: #008080;
+ $xterm-fg-7: #c0c0c0;
+ $xterm-fg-8: #808080;
+ $xterm-fg-9: #f00;
+ $xterm-fg-10: #0f0;
+ $xterm-fg-11: #ff0;
+ $xterm-fg-12: #00f;
+ $xterm-fg-13: #f0f;
+ $xterm-fg-14: #0ff;
+ $xterm-fg-15: $white-light;
+ $xterm-fg-16: $black;
+ $xterm-fg-17: #00005f;
+ $xterm-fg-18: #000087;
+ $xterm-fg-19: #0000af;
+ $xterm-fg-20: #0000d7;
+ $xterm-fg-21: #00f;
+ $xterm-fg-22: #005f00;
+ $xterm-fg-23: #005f5f;
+ $xterm-fg-24: #005f87;
+ $xterm-fg-25: #005faf;
+ $xterm-fg-26: #005fd7;
+ $xterm-fg-27: #005fff;
+ $xterm-fg-28: #008700;
+ $xterm-fg-29: #00875f;
+ $xterm-fg-30: #008787;
+ $xterm-fg-31: #0087af;
+ $xterm-fg-32: #0087d7;
+ $xterm-fg-33: #0087ff;
+ $xterm-fg-34: #00af00;
+ $xterm-fg-35: #00af5f;
+ $xterm-fg-36: #00af87;
+ $xterm-fg-37: #00afaf;
+ $xterm-fg-38: #00afd7;
+ $xterm-fg-39: #00afff;
+ $xterm-fg-40: #00d700;
+ $xterm-fg-41: #00d75f;
+ $xterm-fg-42: #00d787;
+ $xterm-fg-43: #00d7af;
+ $xterm-fg-44: #00d7d7;
+ $xterm-fg-45: #00d7ff;
+ $xterm-fg-46: #0f0;
+ $xterm-fg-47: #00ff5f;
+ $xterm-fg-48: #00ff87;
+ $xterm-fg-49: #00ffaf;
+ $xterm-fg-50: #00ffd7;
+ $xterm-fg-51: #0ff;
+ $xterm-fg-52: #5f0000;
+ $xterm-fg-53: #5f005f;
+ $xterm-fg-54: #5f0087;
+ $xterm-fg-55: #5f00af;
+ $xterm-fg-56: #5f00d7;
+ $xterm-fg-57: #5f00ff;
+ $xterm-fg-58: #5f5f00;
+ $xterm-fg-59: #5f5f5f;
+ $xterm-fg-60: #5f5f87;
+ $xterm-fg-61: #5f5faf;
+ $xterm-fg-62: #5f5fd7;
+ $xterm-fg-63: #5f5fff;
+ $xterm-fg-64: #5f8700;
+ $xterm-fg-65: #5f875f;
+ $xterm-fg-66: #5f8787;
+ $xterm-fg-67: #5f87af;
+ $xterm-fg-68: #5f87d7;
+ $xterm-fg-69: #5f87ff;
+ $xterm-fg-70: #5faf00;
+ $xterm-fg-71: #5faf5f;
+ $xterm-fg-72: #5faf87;
+ $xterm-fg-73: #5fafaf;
+ $xterm-fg-74: #5fafd7;
+ $xterm-fg-75: #5fafff;
+ $xterm-fg-76: #5fd700;
+ $xterm-fg-77: #5fd75f;
+ $xterm-fg-78: #5fd787;
+ $xterm-fg-79: #5fd7af;
+ $xterm-fg-80: #5fd7d7;
+ $xterm-fg-81: #5fd7ff;
+ $xterm-fg-82: #5fff00;
+ $xterm-fg-83: #5fff5f;
+ $xterm-fg-84: #5fff87;
+ $xterm-fg-85: #5fffaf;
+ $xterm-fg-86: #5fffd7;
+ $xterm-fg-87: #5fffff;
+ $xterm-fg-88: #870000;
+ $xterm-fg-89: #87005f;
+ $xterm-fg-90: #870087;
+ $xterm-fg-91: #8700af;
+ $xterm-fg-92: #8700d7;
+ $xterm-fg-93: #8700ff;
+ $xterm-fg-94: #875f00;
+ $xterm-fg-95: #875f5f;
+ $xterm-fg-96: #875f87;
+ $xterm-fg-97: #875faf;
+ $xterm-fg-98: #875fd7;
+ $xterm-fg-99: #875fff;
+ $xterm-fg-100: #878700;
+ $xterm-fg-101: #87875f;
+ $xterm-fg-102: #878787;
+ $xterm-fg-103: #8787af;
+ $xterm-fg-104: #8787d7;
+ $xterm-fg-105: #8787ff;
+ $xterm-fg-106: #87af00;
+ $xterm-fg-107: #87af5f;
+ $xterm-fg-108: #87af87;
+ $xterm-fg-109: #87afaf;
+ $xterm-fg-110: #87afd7;
+ $xterm-fg-111: #87afff;
+ $xterm-fg-112: #87d700;
+ $xterm-fg-113: #87d75f;
+ $xterm-fg-114: #87d787;
+ $xterm-fg-115: #87d7af;
+ $xterm-fg-116: #87d7d7;
+ $xterm-fg-117: #87d7ff;
+ $xterm-fg-118: #87ff00;
+ $xterm-fg-119: #87ff5f;
+ $xterm-fg-120: #87ff87;
+ $xterm-fg-121: #87ffaf;
+ $xterm-fg-122: #87ffd7;
+ $xterm-fg-123: #87ffff;
+ $xterm-fg-124: #af0000;
+ $xterm-fg-125: #af005f;
+ $xterm-fg-126: #af0087;
+ $xterm-fg-127: #af00af;
+ $xterm-fg-128: #af00d7;
+ $xterm-fg-129: #af00ff;
+ $xterm-fg-130: #af5f00;
+ $xterm-fg-131: #af5f5f;
+ $xterm-fg-132: #af5f87;
+ $xterm-fg-133: #af5faf;
+ $xterm-fg-134: #af5fd7;
+ $xterm-fg-135: #af5fff;
+ $xterm-fg-136: #af8700;
+ $xterm-fg-137: #af875f;
+ $xterm-fg-138: #af8787;
+ $xterm-fg-139: #af87af;
+ $xterm-fg-140: #af87d7;
+ $xterm-fg-141: #af87ff;
+ $xterm-fg-142: #afaf00;
+ $xterm-fg-143: #afaf5f;
+ $xterm-fg-144: #afaf87;
+ $xterm-fg-145: #afafaf;
+ $xterm-fg-146: #afafd7;
+ $xterm-fg-147: #afafff;
+ $xterm-fg-148: #afd700;
+ $xterm-fg-149: #afd75f;
+ $xterm-fg-150: #afd787;
+ $xterm-fg-151: #afd7af;
+ $xterm-fg-152: #afd7d7;
+ $xterm-fg-153: #afd7ff;
+ $xterm-fg-154: #afff00;
+ $xterm-fg-155: #afff5f;
+ $xterm-fg-156: #afff87;
+ $xterm-fg-157: #afffaf;
+ $xterm-fg-158: #afffd7;
+ $xterm-fg-159: #afffff;
+ $xterm-fg-160: #d70000;
+ $xterm-fg-161: #d7005f;
+ $xterm-fg-162: #d70087;
+ $xterm-fg-163: #d700af;
+ $xterm-fg-164: #d700d7;
+ $xterm-fg-165: #d700ff;
+ $xterm-fg-166: #d75f00;
+ $xterm-fg-167: #d75f5f;
+ $xterm-fg-168: #d75f87;
+ $xterm-fg-169: #d75faf;
+ $xterm-fg-170: #d75fd7;
+ $xterm-fg-171: #d75fff;
+ $xterm-fg-172: #d78700;
+ $xterm-fg-173: #d7875f;
+ $xterm-fg-174: #d78787;
+ $xterm-fg-175: #d787af;
+ $xterm-fg-176: #d787d7;
+ $xterm-fg-177: #d787ff;
+ $xterm-fg-178: #d7af00;
+ $xterm-fg-179: #d7af5f;
+ $xterm-fg-180: #d7af87;
+ $xterm-fg-181: #d7afaf;
+ $xterm-fg-182: #d7afd7;
+ $xterm-fg-183: #d7afff;
+ $xterm-fg-184: #d7d700;
+ $xterm-fg-185: #d7d75f;
+ $xterm-fg-186: #d7d787;
+ $xterm-fg-187: #d7d7af;
+ $xterm-fg-188: #d7d7d7;
+ $xterm-fg-189: #d7d7ff;
+ $xterm-fg-190: #d7ff00;
+ $xterm-fg-191: #d7ff5f;
+ $xterm-fg-192: #d7ff87;
+ $xterm-fg-193: #d7ffaf;
+ $xterm-fg-194: #d7ffd7;
+ $xterm-fg-195: #d7ffff;
+ $xterm-fg-196: #f00;
+ $xterm-fg-197: #ff005f;
+ $xterm-fg-198: #ff0087;
+ $xterm-fg-199: #ff00af;
+ $xterm-fg-200: #ff00d7;
+ $xterm-fg-201: #f0f;
+ $xterm-fg-202: #ff5f00;
+ $xterm-fg-203: #ff5f5f;
+ $xterm-fg-204: #ff5f87;
+ $xterm-fg-205: #ff5faf;
+ $xterm-fg-206: #ff5fd7;
+ $xterm-fg-207: #ff5fff;
+ $xterm-fg-208: #ff8700;
+ $xterm-fg-209: #ff875f;
+ $xterm-fg-210: #ff8787;
+ $xterm-fg-211: #ff87af;
+ $xterm-fg-212: #ff87d7;
+ $xterm-fg-213: #ff87ff;
+ $xterm-fg-214: #ffaf00;
+ $xterm-fg-215: #ffaf5f;
+ $xterm-fg-216: #ffaf87;
+ $xterm-fg-217: #ffafaf;
+ $xterm-fg-218: #ffafd7;
+ $xterm-fg-219: #ffafff;
+ $xterm-fg-220: #ffd700;
+ $xterm-fg-221: #ffd75f;
+ $xterm-fg-222: #ffd787;
+ $xterm-fg-223: #ffd7af;
+ $xterm-fg-224: #ffd7d7;
+ $xterm-fg-225: #ffd7ff;
+ $xterm-fg-226: #ff0;
+ $xterm-fg-227: #ffff5f;
+ $xterm-fg-228: #ffff87;
+ $xterm-fg-229: #ffffaf;
+ $xterm-fg-230: #ffffd7;
+ $xterm-fg-231: $white-light;
+ $xterm-fg-232: #080808;
+ $xterm-fg-233: #121212;
+ $xterm-fg-234: #1c1c1c;
+ $xterm-fg-235: #262626;
+ $xterm-fg-236: #303030;
+ $xterm-fg-237: #3a3a3a;
+ $xterm-fg-238: #444;
+ $xterm-fg-239: #4e4e4e;
+ $xterm-fg-240: #585858;
+ $xterm-fg-241: #626262;
+ $xterm-fg-242: #6c6c6c;
+ $xterm-fg-243: #767676;
+ $xterm-fg-244: #808080;
+ $xterm-fg-245: #8a8a8a;
+ $xterm-fg-246: #949494;
+ $xterm-fg-247: #9e9e9e;
+ $xterm-fg-248: #a8a8a8;
+ $xterm-fg-249: #b2b2b2;
+ $xterm-fg-250: #bcbcbc;
+ $xterm-fg-251: #c6c6c6;
+ $xterm-fg-252: #d0d0d0;
+ $xterm-fg-253: #dadada;
+ $xterm-fg-254: #e4e4e4;
+ $xterm-fg-255: #eee;
.term-bold {
font-weight: bold;
@@ -169,1026 +429,1026 @@
}
.xterm-fg-0 {
- color: #000;
+ color: $xterm-fg-0;
}
.xterm-fg-1 {
- color: #800000;
+ color: $xterm-fg-1;
}
.xterm-fg-2 {
- color: #008000;
+ color: $xterm-fg-2;
}
.xterm-fg-3 {
- color: #808000;
+ color: $xterm-fg-3;
}
.xterm-fg-4 {
- color: #000080;
+ color: $xterm-fg-4;
}
.xterm-fg-5 {
- color: #800080;
+ color: $xterm-fg-5;
}
.xterm-fg-6 {
- color: #008080;
+ color: $xterm-fg-6;
}
.xterm-fg-7 {
- color: #c0c0c0;
+ color: $xterm-fg-7;
}
.xterm-fg-8 {
- color: #808080;
+ color: $xterm-fg-8;
}
.xterm-fg-9 {
- color: #f00;
+ color: $xterm-fg-9;
}
.xterm-fg-10 {
- color: #0f0;
+ color: $xterm-fg-10;
}
.xterm-fg-11 {
- color: #ff0;
+ color: $xterm-fg-11;
}
.xterm-fg-12 {
- color: #00f;
+ color: $xterm-fg-12;
}
.xterm-fg-13 {
- color: #f0f;
+ color: $xterm-fg-13;
}
.xterm-fg-14 {
- color: #0ff;
+ color: $xterm-fg-14;
}
.xterm-fg-15 {
- color: #fff;
+ color: $white-light;
}
.xterm-fg-16 {
- color: #000;
+ color: $black;
}
.xterm-fg-17 {
- color: #00005f;
+ color: $xterm-fg-17;
}
.xterm-fg-18 {
- color: #000087;
+ color: $xterm-fg-18;
}
.xterm-fg-19 {
- color: #0000af;
+ color: $xterm-fg-19;
}
.xterm-fg-20 {
- color: #0000d7;
+ color: $xterm-fg-20;
}
.xterm-fg-21 {
- color: #00f;
+ color: $xterm-fg-21;
}
.xterm-fg-22 {
- color: #005f00;
+ color: $xterm-fg-22;
}
.xterm-fg-23 {
- color: #005f5f;
+ color: $xterm-fg-23;
}
.xterm-fg-24 {
- color: #005f87;
+ color: $xterm-fg-24;
}
.xterm-fg-25 {
- color: #005faf;
+ color: $xterm-fg-25;
}
.xterm-fg-26 {
- color: #005fd7;
+ color: $xterm-fg-26;
}
.xterm-fg-27 {
- color: #005fff;
+ color: $xterm-fg-27;
}
.xterm-fg-28 {
- color: #008700;
+ color: $xterm-fg-28;
}
.xterm-fg-29 {
- color: #00875f;
+ color: $xterm-fg-29;
}
.xterm-fg-30 {
- color: #008787;
+ color: $xterm-fg-30;
}
.xterm-fg-31 {
- color: #0087af;
+ color: $xterm-fg-31;
}
.xterm-fg-32 {
- color: #0087d7;
+ color: $xterm-fg-32;
}
.xterm-fg-33 {
- color: #0087ff;
+ color: $xterm-fg-33;
}
.xterm-fg-34 {
- color: #00af00;
+ color: $xterm-fg-34;
}
.xterm-fg-35 {
- color: #00af5f;
+ color: $xterm-fg-35;
}
.xterm-fg-36 {
- color: #00af87;
+ color: $xterm-fg-36;
}
.xterm-fg-37 {
- color: #00afaf;
+ color: $xterm-fg-37;
}
.xterm-fg-38 {
- color: #00afd7;
+ color: $xterm-fg-38;
}
.xterm-fg-39 {
- color: #00afff;
+ color: $xterm-fg-39;
}
.xterm-fg-40 {
- color: #00d700;
+ color: $xterm-fg-40;
}
.xterm-fg-41 {
- color: #00d75f;
+ color: $xterm-fg-41;
}
.xterm-fg-42 {
- color: #00d787;
+ color: $xterm-fg-42;
}
.xterm-fg-43 {
- color: #00d7af;
+ color: $xterm-fg-43;
}
.xterm-fg-44 {
- color: #00d7d7;
+ color: $xterm-fg-44;
}
.xterm-fg-45 {
- color: #00d7ff;
+ color: $xterm-fg-45;
}
.xterm-fg-46 {
- color: #0f0;
+ color: $xterm-fg-46;
}
.xterm-fg-47 {
- color: #00ff5f;
+ color: $xterm-fg-47;
}
.xterm-fg-48 {
- color: #00ff87;
+ color: $xterm-fg-48;
}
.xterm-fg-49 {
- color: #00ffaf;
+ color: $xterm-fg-49;
}
.xterm-fg-50 {
- color: #00ffd7;
+ color: $xterm-fg-50;
}
.xterm-fg-51 {
- color: #0ff;
+ color: $xterm-fg-51;
}
.xterm-fg-52 {
- color: #5f0000;
+ color: $xterm-fg-52;
}
.xterm-fg-53 {
- color: #5f005f;
+ color: $xterm-fg-53;
}
.xterm-fg-54 {
- color: #5f0087;
+ color: $xterm-fg-54;
}
.xterm-fg-55 {
- color: #5f00af;
+ color: $xterm-fg-55;
}
.xterm-fg-56 {
- color: #5f00d7;
+ color: $xterm-fg-56;
}
.xterm-fg-57 {
- color: #5f00ff;
+ color: $xterm-fg-57;
}
.xterm-fg-58 {
- color: #5f5f00;
+ color: $xterm-fg-58;
}
.xterm-fg-59 {
- color: #5f5f5f;
+ color: $xterm-fg-59;
}
.xterm-fg-60 {
- color: #5f5f87;
+ color: $xterm-fg-60;
}
.xterm-fg-61 {
- color: #5f5faf;
+ color: $xterm-fg-61;
}
.xterm-fg-62 {
- color: #5f5fd7;
+ color: $xterm-fg-62;
}
.xterm-fg-63 {
- color: #5f5fff;
+ color: $xterm-fg-63;
}
.xterm-fg-64 {
- color: #5f8700;
+ color: $xterm-fg-64;
}
.xterm-fg-65 {
- color: #5f875f;
+ color: $xterm-fg-65;
}
.xterm-fg-66 {
- color: #5f8787;
+ color: $xterm-fg-66;
}
.xterm-fg-67 {
- color: #5f87af;
+ color: $xterm-fg-67;
}
.xterm-fg-68 {
- color: #5f87d7;
+ color: $xterm-fg-68;
}
.xterm-fg-69 {
- color: #5f87ff;
+ color: $xterm-fg-69;
}
.xterm-fg-70 {
- color: #5faf00;
+ color: $xterm-fg-70;
}
.xterm-fg-71 {
- color: #5faf5f;
+ color: $xterm-fg-71;
}
.xterm-fg-72 {
- color: #5faf87;
+ color: $xterm-fg-72;
}
.xterm-fg-73 {
- color: #5fafaf;
+ color: $xterm-fg-73;
}
.xterm-fg-74 {
- color: #5fafd7;
+ color: $xterm-fg-74;
}
.xterm-fg-75 {
- color: #5fafff;
+ color: $xterm-fg-75;
}
.xterm-fg-76 {
- color: #5fd700;
+ color: $xterm-fg-76;
}
.xterm-fg-77 {
- color: #5fd75f;
+ color: $xterm-fg-77;
}
.xterm-fg-78 {
- color: #5fd787;
+ color: $xterm-fg-78;
}
.xterm-fg-79 {
- color: #5fd7af;
+ color: $xterm-fg-79;
}
.xterm-fg-80 {
- color: #5fd7d7;
+ color: $xterm-fg-80;
}
.xterm-fg-81 {
- color: #5fd7ff;
+ color: $xterm-fg-81;
}
.xterm-fg-82 {
- color: #5fff00;
+ color: $xterm-fg-82;
}
.xterm-fg-83 {
- color: #5fff5f;
+ color: $xterm-fg-83;
}
.xterm-fg-84 {
- color: #5fff87;
+ color: $xterm-fg-84;
}
.xterm-fg-85 {
- color: #5fffaf;
+ color: $xterm-fg-85;
}
.xterm-fg-86 {
- color: #5fffd7;
+ color: $xterm-fg-86;
}
.xterm-fg-87 {
- color: #5fffff;
+ color: $xterm-fg-87;
}
.xterm-fg-88 {
- color: #870000;
+ color: $xterm-fg-88;
}
.xterm-fg-89 {
- color: #87005f;
+ color: $xterm-fg-89;
}
.xterm-fg-90 {
- color: #870087;
+ color: $xterm-fg-90;
}
.xterm-fg-91 {
- color: #8700af;
+ color: $xterm-fg-91;
}
.xterm-fg-92 {
- color: #8700d7;
+ color: $xterm-fg-92;
}
.xterm-fg-93 {
- color: #8700ff;
+ color: $xterm-fg-93;
}
.xterm-fg-94 {
- color: #875f00;
+ color: $xterm-fg-94;
}
.xterm-fg-95 {
- color: #875f5f;
+ color: $xterm-fg-95;
}
.xterm-fg-96 {
- color: #875f87;
+ color: $xterm-fg-96;
}
.xterm-fg-97 {
- color: #875faf;
+ color: $xterm-fg-97;
}
.xterm-fg-98 {
- color: #875fd7;
+ color: $xterm-fg-98;
}
.xterm-fg-99 {
- color: #875fff;
+ color: $xterm-fg-99;
}
.xterm-fg-100 {
- color: #878700;
+ color: $xterm-fg-100;
}
.xterm-fg-101 {
- color: #87875f;
+ color: $xterm-fg-101;
}
.xterm-fg-102 {
- color: #878787;
+ color: $xterm-fg-102;
}
.xterm-fg-103 {
- color: #8787af;
+ color: $xterm-fg-103;
}
.xterm-fg-104 {
- color: #8787d7;
+ color: $xterm-fg-104;
}
.xterm-fg-105 {
- color: #8787ff;
+ color: $xterm-fg-105;
}
.xterm-fg-106 {
- color: #87af00;
+ color: $xterm-fg-106;
}
.xterm-fg-107 {
- color: #87af5f;
+ color: $xterm-fg-107;
}
.xterm-fg-108 {
- color: #87af87;
+ color: $xterm-fg-108;
}
.xterm-fg-109 {
- color: #87afaf;
+ color: $xterm-fg-109;
}
.xterm-fg-110 {
- color: #87afd7;
+ color: $xterm-fg-110;
}
.xterm-fg-111 {
- color: #87afff;
+ color: $xterm-fg-111;
}
.xterm-fg-112 {
- color: #87d700;
+ color: $xterm-fg-112;
}
.xterm-fg-113 {
- color: #87d75f;
+ color: $xterm-fg-113;
}
.xterm-fg-114 {
- color: #87d787;
+ color: $xterm-fg-114;
}
.xterm-fg-115 {
- color: #87d7af;
+ color: $xterm-fg-115;
}
.xterm-fg-116 {
- color: #87d7d7;
+ color: $xterm-fg-116;
}
.xterm-fg-117 {
- color: #87d7ff;
+ color: $xterm-fg-117;
}
.xterm-fg-118 {
- color: #87ff00;
+ color: $xterm-fg-118;
}
.xterm-fg-119 {
- color: #87ff5f;
+ color: $xterm-fg-119;
}
.xterm-fg-120 {
- color: #87ff87;
+ color: $xterm-fg-120;
}
.xterm-fg-121 {
- color: #87ffaf;
+ color: $xterm-fg-121;
}
.xterm-fg-122 {
- color: #87ffd7;
+ color: $xterm-fg-122;
}
.xterm-fg-123 {
- color: #87ffff;
+ color: $xterm-fg-123;
}
.xterm-fg-124 {
- color: #af0000;
+ color: $xterm-fg-124;
}
.xterm-fg-125 {
- color: #af005f;
+ color: $xterm-fg-125;
}
.xterm-fg-126 {
- color: #af0087;
+ color: $xterm-fg-126;
}
.xterm-fg-127 {
- color: #af00af;
+ color: $xterm-fg-127;
}
.xterm-fg-128 {
- color: #af00d7;
+ color: $xterm-fg-128;
}
.xterm-fg-129 {
- color: #af00ff;
+ color: $xterm-fg-129;
}
.xterm-fg-130 {
- color: #af5f00;
+ color: $xterm-fg-130;
}
.xterm-fg-131 {
- color: #af5f5f;
+ color: $xterm-fg-131;
}
.xterm-fg-132 {
- color: #af5f87;
+ color: $xterm-fg-132;
}
.xterm-fg-133 {
- color: #af5faf;
+ color: $xterm-fg-133;
}
.xterm-fg-134 {
- color: #af5fd7;
+ color: $xterm-fg-134;
}
.xterm-fg-135 {
- color: #af5fff;
+ color: $xterm-fg-135;
}
.xterm-fg-136 {
- color: #af8700;
+ color: $xterm-fg-136;
}
.xterm-fg-137 {
- color: #af875f;
+ color: $xterm-fg-137;
}
.xterm-fg-138 {
- color: #af8787;
+ color: $xterm-fg-138;
}
.xterm-fg-139 {
- color: #af87af;
+ color: $xterm-fg-139;
}
.xterm-fg-140 {
- color: #af87d7;
+ color: $xterm-fg-140;
}
.xterm-fg-141 {
- color: #af87ff;
+ color: $xterm-fg-141;
}
.xterm-fg-142 {
- color: #afaf00;
+ color: $xterm-fg-142;
}
.xterm-fg-143 {
- color: #afaf5f;
+ color: $xterm-fg-143;
}
.xterm-fg-144 {
- color: #afaf87;
+ color: $xterm-fg-144;
}
.xterm-fg-145 {
- color: #afafaf;
+ color: $xterm-fg-145;
}
.xterm-fg-146 {
- color: #afafd7;
+ color: $xterm-fg-146;
}
.xterm-fg-147 {
- color: #afafff;
+ color: $xterm-fg-147;
}
.xterm-fg-148 {
- color: #afd700;
+ color: $xterm-fg-148;
}
.xterm-fg-149 {
- color: #afd75f;
+ color: $xterm-fg-149;
}
.xterm-fg-150 {
- color: #afd787;
+ color: $xterm-fg-150;
}
.xterm-fg-151 {
- color: #afd7af;
+ color: $xterm-fg-151;
}
.xterm-fg-152 {
- color: #afd7d7;
+ color: $xterm-fg-152;
}
.xterm-fg-153 {
- color: #afd7ff;
+ color: $xterm-fg-153;
}
.xterm-fg-154 {
- color: #afff00;
+ color: $xterm-fg-154;
}
.xterm-fg-155 {
- color: #afff5f;
+ color: $xterm-fg-155;
}
.xterm-fg-156 {
- color: #afff87;
+ color: $xterm-fg-156;
}
.xterm-fg-157 {
- color: #afffaf;
+ color: $xterm-fg-157;
}
.xterm-fg-158 {
- color: #afffd7;
+ color: $xterm-fg-158;
}
.xterm-fg-159 {
- color: #afffff;
+ color: $xterm-fg-159;
}
.xterm-fg-160 {
- color: #d70000;
+ color: $xterm-fg-160;
}
.xterm-fg-161 {
- color: #d7005f;
+ color: $xterm-fg-161;
}
.xterm-fg-162 {
- color: #d70087;
+ color: $xterm-fg-162;
}
.xterm-fg-163 {
- color: #d700af;
+ color: $xterm-fg-163;
}
.xterm-fg-164 {
- color: #d700d7;
+ color: $xterm-fg-164;
}
.xterm-fg-165 {
- color: #d700ff;
+ color: $xterm-fg-165;
}
.xterm-fg-166 {
- color: #d75f00;
+ color: $xterm-fg-166;
}
.xterm-fg-167 {
- color: #d75f5f;
+ color: $xterm-fg-167;
}
.xterm-fg-168 {
- color: #d75f87;
+ color: $xterm-fg-168;
}
.xterm-fg-169 {
- color: #d75faf;
+ color: $xterm-fg-169;
}
.xterm-fg-170 {
- color: #d75fd7;
+ color: $xterm-fg-170;
}
.xterm-fg-171 {
- color: #d75fff;
+ color: $xterm-fg-171;
}
.xterm-fg-172 {
- color: #d78700;
+ color: $xterm-fg-172;
}
.xterm-fg-173 {
- color: #d7875f;
+ color: $xterm-fg-173;
}
.xterm-fg-174 {
- color: #d78787;
+ color: $xterm-fg-174;
}
.xterm-fg-175 {
- color: #d787af;
+ color: $xterm-fg-175;
}
.xterm-fg-176 {
- color: #d787d7;
+ color: $xterm-fg-176;
}
.xterm-fg-177 {
- color: #d787ff;
+ color: $xterm-fg-177;
}
.xterm-fg-178 {
- color: #d7af00;
+ color: $xterm-fg-178;
}
.xterm-fg-179 {
- color: #d7af5f;
+ color: $xterm-fg-179;
}
.xterm-fg-180 {
- color: #d7af87;
+ color: $xterm-fg-180;
}
.xterm-fg-181 {
- color: #d7afaf;
+ color: $xterm-fg-181;
}
.xterm-fg-182 {
- color: #d7afd7;
+ color: $xterm-fg-182;
}
.xterm-fg-183 {
- color: #d7afff;
+ color: $xterm-fg-183;
}
.xterm-fg-184 {
- color: #d7d700;
+ color: $xterm-fg-184;
}
.xterm-fg-185 {
- color: #d7d75f;
+ color: $xterm-fg-185;
}
.xterm-fg-186 {
- color: #d7d787;
+ color: $xterm-fg-186;
}
.xterm-fg-187 {
- color: #d7d7af;
+ color: $xterm-fg-187;
}
.xterm-fg-188 {
- color: #d7d7d7;
+ color: $xterm-fg-188;
}
.xterm-fg-189 {
- color: #d7d7ff;
+ color: $xterm-fg-189;
}
.xterm-fg-190 {
- color: #d7ff00;
+ color: $xterm-fg-190;
}
.xterm-fg-191 {
- color: #d7ff5f;
+ color: $xterm-fg-191;
}
.xterm-fg-192 {
- color: #d7ff87;
+ color: $xterm-fg-192;
}
.xterm-fg-193 {
- color: #d7ffaf;
+ color: $xterm-fg-193;
}
.xterm-fg-194 {
- color: #d7ffd7;
+ color: $xterm-fg-194;
}
.xterm-fg-195 {
- color: #d7ffff;
+ color: $xterm-fg-195;
}
.xterm-fg-196 {
- color: #f00;
+ color: $xterm-fg-196;
}
.xterm-fg-197 {
- color: #ff005f;
+ color: $xterm-fg-197;
}
.xterm-fg-198 {
- color: #ff0087;
+ color: $xterm-fg-198;
}
.xterm-fg-199 {
- color: #ff00af;
+ color: $xterm-fg-199;
}
.xterm-fg-200 {
- color: #ff00d7;
+ color: $xterm-fg-200;
}
.xterm-fg-201 {
- color: #f0f;
+ color: $xterm-fg-201;
}
.xterm-fg-202 {
- color: #ff5f00;
+ color: $xterm-fg-202;
}
.xterm-fg-203 {
- color: #ff5f5f;
+ color: $xterm-fg-203;
}
.xterm-fg-204 {
- color: #ff5f87;
+ color: $xterm-fg-204;
}
.xterm-fg-205 {
- color: #ff5faf;
+ color: $xterm-fg-205;
}
.xterm-fg-206 {
- color: #ff5fd7;
+ color: $xterm-fg-206;
}
.xterm-fg-207 {
- color: #ff5fff;
+ color: $xterm-fg-207;
}
.xterm-fg-208 {
- color: #ff8700;
+ color: $xterm-fg-208;
}
.xterm-fg-209 {
- color: #ff875f;
+ color: $xterm-fg-209;
}
.xterm-fg-210 {
- color: #ff8787;
+ color: $xterm-fg-210;
}
.xterm-fg-211 {
- color: #ff87af;
+ color: $xterm-fg-211;
}
.xterm-fg-212 {
- color: #ff87d7;
+ color: $xterm-fg-212;
}
.xterm-fg-213 {
- color: #ff87ff;
+ color: $xterm-fg-213;
}
.xterm-fg-214 {
- color: #ffaf00;
+ color: $xterm-fg-214;
}
.xterm-fg-215 {
- color: #ffaf5f;
+ color: $xterm-fg-215;
}
.xterm-fg-216 {
- color: #ffaf87;
+ color: $xterm-fg-216;
}
.xterm-fg-217 {
- color: #ffafaf;
+ color: $xterm-fg-217;
}
.xterm-fg-218 {
- color: #ffafd7;
+ color: $xterm-fg-218;
}
.xterm-fg-219 {
- color: #ffafff;
+ color: $xterm-fg-219;
}
.xterm-fg-220 {
- color: #ffd700;
+ color: $xterm-fg-220;
}
.xterm-fg-221 {
- color: #ffd75f;
+ color: $xterm-fg-221;
}
.xterm-fg-222 {
- color: #ffd787;
+ color: $xterm-fg-222;
}
.xterm-fg-223 {
- color: #ffd7af;
+ color: $xterm-fg-223;
}
.xterm-fg-224 {
- color: #ffd7d7;
+ color: $xterm-fg-224;
}
.xterm-fg-225 {
- color: #ffd7ff;
+ color: $xterm-fg-225;
}
.xterm-fg-226 {
- color: #ff0;
+ color: $xterm-fg-226;
}
.xterm-fg-227 {
- color: #ffff5f;
+ color: $xterm-fg-227;
}
.xterm-fg-228 {
- color: #ffff87;
+ color: $xterm-fg-228;
}
.xterm-fg-229 {
- color: #ffffaf;
+ color: $xterm-fg-229;
}
.xterm-fg-230 {
- color: #ffffd7;
+ color: $xterm-fg-230;
}
.xterm-fg-231 {
- color: #fff;
+ color: $xterm-fg-231;
}
.xterm-fg-232 {
- color: #080808;
+ color: $xterm-fg-232;
}
.xterm-fg-233 {
- color: #121212;
+ color: $xterm-fg-233;
}
.xterm-fg-234 {
- color: #1c1c1c;
+ color: $xterm-fg-234;
}
.xterm-fg-235 {
- color: #262626;
+ color: $xterm-fg-235;
}
.xterm-fg-236 {
- color: #303030;
+ color: $xterm-fg-236;
}
.xterm-fg-237 {
- color: #3a3a3a;
+ color: $xterm-fg-237;
}
.xterm-fg-238 {
- color: #444;
+ color: $xterm-fg-238;
}
.xterm-fg-239 {
- color: #4e4e4e;
+ color: $xterm-fg-239;
}
.xterm-fg-240 {
- color: #585858;
+ color: $xterm-fg-240;
}
.xterm-fg-241 {
- color: #626262;
+ color: $xterm-fg-241;
}
.xterm-fg-242 {
- color: #6c6c6c;
+ color: $xterm-fg-242;
}
.xterm-fg-243 {
- color: #767676;
+ color: $xterm-fg-243;
}
.xterm-fg-244 {
- color: #808080;
+ color: $xterm-fg-244;
}
.xterm-fg-245 {
- color: #8a8a8a;
+ color: $xterm-fg-245;
}
.xterm-fg-246 {
- color: #949494;
+ color: $xterm-fg-246;
}
.xterm-fg-247 {
- color: #9e9e9e;
+ color: $xterm-fg-247;
}
.xterm-fg-248 {
- color: #a8a8a8;
+ color: $xterm-fg-248;
}
.xterm-fg-249 {
- color: #b2b2b2;
+ color: $xterm-fg-249;
}
.xterm-fg-250 {
- color: #bcbcbc;
+ color: $xterm-fg-250;
}
.xterm-fg-251 {
- color: #c6c6c6;
+ color: $xterm-fg-251;
}
.xterm-fg-252 {
- color: #d0d0d0;
+ color: $xterm-fg-252;
}
.xterm-fg-253 {
- color: #dadada;
+ color: $xterm-fg-253;
}
.xterm-fg-254 {
- color: #e4e4e4;
+ color: $xterm-fg-254;
}
.xterm-fg-255 {
- color: #eee;
+ color: $xterm-fg-255;
}
}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 52e0256943a..c2bb8464824 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -112,11 +112,14 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:koding_enabled,
:koding_url,
:email_author_in_body,
+ :html_emails_enabled,
:repository_checks_enabled,
:metrics_packet_size,
:send_user_confirmation_email,
:container_registry_token_expire_delay,
:enabled_git_access_protocol,
+ :sidekiq_throttling_enabled,
+ :sidekiq_throttling_factor,
:housekeeping_enabled,
:housekeeping_bitmaps_enabled,
:housekeeping_incremental_repack_period,
@@ -125,7 +128,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
repository_storages: [],
restricted_visibility_levels: [],
import_sources: [],
- disabled_oauth_sign_in_sources: []
+ disabled_oauth_sign_in_sources: [],
+ sidekiq_throttling_queues: []
)
end
end
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index 471d24934a0..62f62e99a97 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -1,5 +1,8 @@
class Admin::ApplicationsController < Admin::ApplicationController
+ include OauthApplications
+
before_action :set_application, only: [:show, :edit, :update, :destroy]
+ before_action :load_scopes, only: [:new, :edit]
def index
@applications = Doorkeeper::Application.where("owner_id IS NULL")
@@ -47,6 +50,6 @@ class Admin::ApplicationsController < Admin::ApplicationController
# Only allow a trusted parameter "white list" through.
def application_params
- params[:doorkeeper_application].permit(:name, :redirect_uri)
+ params[:doorkeeper_application].permit(:name, :redirect_uri, :scopes)
end
end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index aa7570cd896..1e3d194e9f9 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -56,7 +56,7 @@ class Admin::GroupsController < Admin::ApplicationController
private
def group
- @group ||= Group.find_by(path: params[:id])
+ @group ||= Group.find_by_full_path(params[:id])
end
def group_params
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 517ad4f03f3..4df80195ae1 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -49,6 +49,14 @@ class ApplicationController < ActionController::Base
render_404
end
+ def route_not_found
+ if current_user
+ not_found
+ else
+ redirect_to new_user_session_path
+ end
+ end
+
protected
# This filter handles both private tokens and personal access tokens
@@ -224,7 +232,7 @@ class ApplicationController < ActionController::Base
end
def require_email
- if current_user && current_user.temp_oauth_email?
+ if current_user && current_user.temp_oauth_email? && session[:impersonator_id].nil?
redirect_to profile_path, notice: 'Please complete your profile with email address' and return
end
end
@@ -254,7 +262,7 @@ class ApplicationController < ActionController::Base
end
def bitbucket_import_configured?
- Gitlab::OAuth::Provider.enabled?(:bitbucket) && Gitlab::BitbucketImport.public_key.present?
+ Gitlab::OAuth::Provider.enabled?(:bitbucket)
end
def google_code_import_enabled?
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index daa82336208..5f13353baa1 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -11,7 +11,7 @@ class AutocompleteController < ApplicationController
@users = @users.reorder(:name)
@users = @users.page(params[:page])
- if params[:todo_filter].present?
+ if params[:todo_filter].present? && current_user
@users = @users.todo_authors(current_user.id, params[:todo_state_filter])
end
@@ -55,7 +55,13 @@ class AutocompleteController < ApplicationController
def find_users
@users =
if @project
- @project.team.users
+ user_ids = @project.team.users.pluck(:id)
+
+ if params[:author_id].present?
+ user_ids << params[:author_id]
+ end
+
+ User.where(id: user_ids)
elsif params[:group_id].present?
group = Group.find(params[:group_id])
return render_404 unless can?(current_user, :read_group, group)
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index dacb5679dd3..936d9bab57e 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -81,10 +81,8 @@ module CreatesCommit
def merge_request_exists?
return @merge_request if defined?(@merge_request)
- @merge_request = @mr_target_project.merge_requests.opened.find_by(
- source_branch: @mr_source_branch,
- target_branch: @mr_target_branch
- )
+ @merge_request = MergeRequestsFinder.new(current_user, project_id: @mr_target_project.id).execute.opened.
+ find_by(source_branch: @mr_source_branch, target_branch: @mr_target_branch)
end
def different_project?
diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb
new file mode 100644
index 00000000000..2aaf8f2b451
--- /dev/null
+++ b/app/controllers/concerns/cycle_analytics_params.rb
@@ -0,0 +1,7 @@
+module CycleAnalyticsParams
+ extend ActiveSupport::Concern
+
+ def start_date(params)
+ params[:start_date] == '30' ? 30.days.ago : 90.days.ago
+ end
+end
diff --git a/app/controllers/concerns/diff_for_path.rb b/app/controllers/concerns/diff_for_path.rb
index aeec3009f15..1efa9fe060f 100644
--- a/app/controllers/concerns/diff_for_path.rb
+++ b/app/controllers/concerns/diff_for_path.rb
@@ -3,7 +3,7 @@ module DiffForPath
def render_diff_for_path(diffs)
diff_file = diffs.diff_files.find do |diff|
- diff.old_path == params[:old_path] && diff.new_path == params[:new_path]
+ diff.file_identifier == params[:file_identifier]
end
return render_404 unless diff_file
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index be86fa106f8..0821974aa93 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -12,7 +12,7 @@ module IssuableActions
destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym
TodoService.new.public_send(destroy_method, issuable, current_user)
- name = issuable.class.name.titleize.downcase
+ name = issuable.human_class_name
flash[:notice] = "The #{name} was successfully deleted."
redirect_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index b5e79099e39..6247934f81e 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -10,11 +10,11 @@ module IssuableCollections
private
def issues_collection
- issues_finder.execute
+ issues_finder.execute.preload(:project, :author, :assignee, :labels, :milestone, project: :namespace)
end
def merge_requests_collection
- merge_requests_finder.execute
+ merge_requests_finder.execute.preload(:source_project, :target_project, :author, :assignee, :labels, :milestone, :merge_request_diff, target_project: :namespace)
end
def issues_finder
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index b89fb94be6e..b46adcceb60 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -7,7 +7,6 @@ module IssuesAction
@issues = issues_collection
.non_archived
- .preload(:author, :project)
.page(params[:page])
respond_to do |format|
diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb
new file mode 100644
index 00000000000..ed22b1e5470
--- /dev/null
+++ b/app/controllers/concerns/lfs_request.rb
@@ -0,0 +1,109 @@
+# This concern assumes:
+# - a `#project` accessor
+# - a `#user` accessor
+# - a `#authentication_result` accessor
+# - a `#can?(object, action, subject)` method
+# - a `#ci?` method
+# - a `#download_request?` method
+# - a `#upload_request?` method
+# - a `#has_authentication_ability?(ability)` method
+module LfsRequest
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :require_lfs_enabled!
+ before_action :lfs_check_access!
+ end
+
+ private
+
+ def require_lfs_enabled!
+ return if Gitlab.config.lfs.enabled
+
+ render(
+ json: {
+ message: 'Git LFS is not enabled on this GitLab server, contact your admin.',
+ documentation_url: help_url,
+ },
+ status: 501
+ )
+ end
+
+ def lfs_check_access!
+ return if download_request? && lfs_download_access?
+ return if upload_request? && lfs_upload_access?
+
+ if project.public? || can?(user, :read_project, project)
+ lfs_forbidden!
+ else
+ render_lfs_not_found
+ end
+ end
+
+ def lfs_forbidden!
+ render_lfs_forbidden
+ end
+
+ def render_lfs_forbidden
+ render(
+ json: {
+ message: 'Access forbidden. Check your access level.',
+ documentation_url: help_url,
+ },
+ content_type: "application/vnd.git-lfs+json",
+ status: 403
+ )
+ end
+
+ def render_lfs_not_found
+ render(
+ json: {
+ message: 'Not found.',
+ documentation_url: help_url,
+ },
+ content_type: "application/vnd.git-lfs+json",
+ status: 404
+ )
+ end
+
+ def lfs_download_access?
+ return false unless project.lfs_enabled?
+
+ ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code?
+ end
+
+ def lfs_upload_access?
+ return false unless project.lfs_enabled?
+
+ has_authentication_ability?(:push_code) && can?(user, :push_code, project)
+ end
+
+ def lfs_deploy_token?
+ authentication_result.lfs_deploy_token?(project)
+ end
+
+ def user_can_download_code?
+ has_authentication_ability?(:download_code) && can?(user, :download_code, project)
+ end
+
+ def build_can_download_code?
+ has_authentication_ability?(:build_download_code) && can?(user, :build_download_code, project)
+ end
+
+ def storage_project
+ @storage_project ||= begin
+ result = project
+
+ loop do
+ break unless result.forked?
+ result = result.forked_from_project
+ end
+
+ result
+ end
+ end
+
+ def objects
+ @objects ||= (params[:objects] || []).to_a
+ end
+end
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index a1b0eee37f9..fdb05bb3228 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -6,8 +6,12 @@ module MergeRequestsAction
@label = merge_requests_finder.labels.first
@merge_requests = merge_requests_collection
- .non_archived
- .preload(:author, :target_project)
.page(params[:page])
end
+
+ private
+
+ def filter_params
+ super.merge(non_archived: true)
+ end
end
diff --git a/app/controllers/concerns/oauth_applications.rb b/app/controllers/concerns/oauth_applications.rb
new file mode 100644
index 00000000000..9849aa93fa6
--- /dev/null
+++ b/app/controllers/concerns/oauth_applications.rb
@@ -0,0 +1,19 @@
+module OauthApplications
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :prepare_scopes, only: [:create, :update]
+ end
+
+ def prepare_scopes
+ scopes = params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil)
+
+ if scopes
+ params[:doorkeeper_application][:scopes] = scopes.join(' ')
+ end
+ end
+
+ def load_scopes
+ @scopes = Doorkeeper.configuration.scopes
+ end
+end
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index c33d7eecb9f..549a8526715 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -18,7 +18,7 @@ module ServiceParams
:add_pusher, :send_from_committer_email, :disable_diffs,
:external_wiki_url, :notify, :color,
:server_host, :server_port, :default_irc_uri, :enable_ssl_verification,
- :jira_issue_transition_id, :url, :project_key]
+ :jira_issue_transition_id, :url, :project_key, :ca_pem, :namespace]
# Parameters to ignore if no value is specified
FILTER_BLANK_PARAMS = [:password]
diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb
index 3717c49f272..fbf9a026b10 100644
--- a/app/controllers/concerns/toggle_award_emoji.rb
+++ b/app/controllers/concerns/toggle_award_emoji.rb
@@ -1,11 +1,8 @@
module ToggleAwardEmoji
extend ActiveSupport::Concern
- included do
- before_action :authenticate_user!, only: [:toggle_award_emoji]
- end
-
def toggle_award_emoji
+ authenticate_user!
name = params.require(:name)
if awardable.user_can_award?(current_user, name)
diff --git a/app/controllers/concerns/toggle_subscription_action.rb b/app/controllers/concerns/toggle_subscription_action.rb
index 9e3b9be2ff4..92cb534343e 100644
--- a/app/controllers/concerns/toggle_subscription_action.rb
+++ b/app/controllers/concerns/toggle_subscription_action.rb
@@ -4,13 +4,17 @@ module ToggleSubscriptionAction
def toggle_subscription
return unless current_user
- subscribable_resource.toggle_subscription(current_user)
+ subscribable_resource.toggle_subscription(current_user, subscribable_project)
head :ok
end
private
+ def subscribable_project
+ @project || raise(NotImplementedError)
+ end
+
def subscribable_resource
raise NotImplementedError
end
diff --git a/app/controllers/concerns/workhorse_request.rb b/app/controllers/concerns/workhorse_request.rb
new file mode 100644
index 00000000000..43c0f1b173c
--- /dev/null
+++ b/app/controllers/concerns/workhorse_request.rb
@@ -0,0 +1,13 @@
+module WorkhorseRequest
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :verify_workhorse_api!
+ end
+
+ private
+
+ def verify_workhorse_api!
+ Gitlab::Workhorse.verify_api_request!(request.headers)
+ end
+end
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index 949b4a6c25a..c411c21bb80 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -9,7 +9,7 @@ class Groups::ApplicationController < ApplicationController
def group
unless @group
id = params[:group_id] || params[:id]
- @group = Group.find_by(path: id)
+ @group = Group.find_by_full_path(id)
unless @group && can?(current_user, :read_group, @group)
@group = nil
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 940a3ad20ba..4f273a8d4f0 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -1,20 +1,20 @@
class Groups::GroupMembersController < Groups::ApplicationController
include MembershipActions
+ include SortingHelper
# Authorize
before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
def index
+ @sort = params[:sort].presence || sort_value_name
@project = @group.projects.find(params[:project_id]) if params[:project_id]
+
@members = @group.group_members
@members = @members.non_invite unless can?(current_user, :admin_group, @group)
+ @members = @members.search(params[:search]) if params[:search].present?
+ @members = @members.sort(@sort)
+ @members = @members.page(params[:page]).per(50)
- if params[:search].present?
- users = @group.users.search(params[:search]).to_a
- @members = @members.where(user_id: users)
- end
-
- @members = @members.order('access_level DESC').page(params[:page]).per(50)
@requesters = AccessRequestsFinder.new(@group).execute(current_user)
@group_member = @group.group_members.new
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
index 29528b2cfaa..587898a8634 100644
--- a/app/controllers/groups/labels_controller.rb
+++ b/app/controllers/groups/labels_controller.rb
@@ -1,4 +1,6 @@
class Groups::LabelsController < Groups::ApplicationController
+ include ToggleSubscriptionAction
+
before_action :label, only: [:edit, :update, :destroy]
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy]
before_action :save_previous_label_path, only: [:edit]
@@ -69,6 +71,11 @@ class Groups::LabelsController < Groups::ApplicationController
def label
@label ||= @group.labels.find(params[:id])
end
+ alias_method :subscribable_resource, :label
+
+ def subscribable_project
+ nil
+ end
def label_params
params.require(:label).permit(:title, :description, :color)
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 9d5a28e8d4d..24ec4eec3f2 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -58,7 +58,7 @@ class Groups::MilestonesController < Groups::ApplicationController
def render_new_with_error(empty_project_ids)
@milestone = Milestone.new(milestone_params)
- @milestone.errors.add(:project_id, "Please select at least one project.") if empty_project_ids
+ @milestone.errors.add(:base, "Please select at least one project.") if empty_project_ids
render :new
end
@@ -67,7 +67,7 @@ class Groups::MilestonesController < Groups::ApplicationController
end
def milestone_params
- params.require(:milestone).permit(:title, :description, :due_date, :state_event)
+ params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
end
def milestone_path(title)
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index 4eca278599f..37feff79999 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -6,9 +6,11 @@ class HelpController < ApplicationController
def index
@help_index = File.read(Rails.root.join('doc', 'README.md'))
- # Prefix Markdown links with `help/` unless they already have been
- # See http://rubular.com/r/nwwhzH6Z8X
- @help_index.gsub!(/(\]\()(?!help\/)([^\)\(]+)(\))/, '\1help/\2\3')
+ # Prefix Markdown links with `help/` unless they are external links
+ # See http://rubular.com/r/X3baHTbPO2
+ @help_index.gsub!(%r{(?<delim>\]\()(?!.+://)(?!/)(?<link>[^\)\(]+\))}) do
+ "#{$~[:delim]}#{Gitlab.config.gitlab.relative_url_root}/help/#{$~[:link]}"
+ end
end
def show
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 6ea54744da8..8e42cdf415f 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -2,50 +2,57 @@ class Import::BitbucketController < Import::BaseController
before_action :verify_bitbucket_import_enabled
before_action :bitbucket_auth, except: :callback
- rescue_from OAuth::Error, with: :bitbucket_unauthorized
- rescue_from Gitlab::BitbucketImport::Client::Unauthorized, with: :bitbucket_unauthorized
+ rescue_from OAuth2::Error, with: :bitbucket_unauthorized
+ rescue_from Bitbucket::Error::Unauthorized, with: :bitbucket_unauthorized
def callback
- request_token = session.delete(:oauth_request_token)
- raise "Session expired!" if request_token.nil?
+ response = client.auth_code.get_token(params[:code], redirect_uri: callback_import_bitbucket_url)
- request_token.symbolize_keys!
-
- access_token = client.get_token(request_token, params[:oauth_verifier], callback_import_bitbucket_url)
-
- session[:bitbucket_access_token] = access_token.token
- session[:bitbucket_access_token_secret] = access_token.secret
+ session[:bitbucket_token] = response.token
+ session[:bitbucket_expires_at] = response.expires_at
+ session[:bitbucket_expires_in] = response.expires_in
+ session[:bitbucket_refresh_token] = response.refresh_token
redirect_to status_import_bitbucket_url
end
def status
- @repos = client.projects
- @incompatible_repos = client.incompatible_projects
+ bitbucket_client = Bitbucket::Client.new(credentials)
+ repos = bitbucket_client.repos
+
+ @repos, @incompatible_repos = repos.partition { |repo| repo.valid? }
- @already_added_projects = current_user.created_projects.where(import_type: "bitbucket")
+ @already_added_projects = current_user.created_projects.where(import_type: 'bitbucket')
already_added_projects_names = @already_added_projects.pluck(:import_source)
- @repos.to_a.reject!{ |repo| already_added_projects_names.include? "#{repo["owner"]}/#{repo["slug"]}" }
+ @repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.full_name) }
end
def jobs
- jobs = current_user.created_projects.where(import_type: "bitbucket").to_json(only: [:id, :import_status])
- render json: jobs
+ render json: current_user.created_projects
+ .where(import_type: 'bitbucket')
+ .to_json(only: [:id, :import_status])
end
def create
+ bitbucket_client = Bitbucket::Client.new(credentials)
+
@repo_id = params[:repo_id].to_s
- repo = client.project(@repo_id.gsub('___', '/'))
- @project_name = repo['slug']
- @target_namespace = find_or_create_namespace(repo['owner'], client.user['user']['username'])
+ name = @repo_id.gsub('___', '/')
+ repo = bitbucket_client.repo(name)
+ @project_name = params[:new_name].presence || repo.name
- unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user, access_params).execute
- render 'deploy_key' and return
- end
+ repo_owner = repo.owner
+ repo_owner = current_user.username if repo_owner == bitbucket_client.user.username
+ @target_namespace = params[:new_namespace].presence || repo_owner
+
+ namespace = find_or_create_namespace(@target_namespace, current_user)
- if current_user.can?(:create_projects, @target_namespace)
- @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute
+ if current_user.can?(:create_projects, namespace)
+ # The token in a session can be expired, we need to get most recent one because
+ # Bitbucket::Connection class refreshes it.
+ session[:bitbucket_token] = bitbucket_client.connection.token
+ @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @project_name, namespace, current_user, credentials).execute
else
render 'unauthorized'
end
@@ -54,8 +61,15 @@ class Import::BitbucketController < Import::BaseController
private
def client
- @client ||= Gitlab::BitbucketImport::Client.new(session[:bitbucket_access_token],
- session[:bitbucket_access_token_secret])
+ @client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options)
+ end
+
+ def provider
+ Gitlab::OAuth::Provider.config_for('bitbucket')
+ end
+
+ def options
+ OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys
end
def verify_bitbucket_import_enabled
@@ -63,26 +77,23 @@ class Import::BitbucketController < Import::BaseController
end
def bitbucket_auth
- if session[:bitbucket_access_token].blank?
- go_to_bitbucket_for_permissions
- end
+ go_to_bitbucket_for_permissions if session[:bitbucket_token].blank?
end
def go_to_bitbucket_for_permissions
- request_token = client.request_token(callback_import_bitbucket_url)
- session[:oauth_request_token] = request_token
-
- redirect_to client.authorize_url(request_token, callback_import_bitbucket_url)
+ redirect_to client.auth_code.authorize_url(redirect_uri: callback_import_bitbucket_url)
end
def bitbucket_unauthorized
go_to_bitbucket_for_permissions
end
- def access_params
+ def credentials
{
- bitbucket_access_token: session[:bitbucket_access_token],
- bitbucket_access_token_secret: session[:bitbucket_access_token_secret]
+ token: session[:bitbucket_token],
+ expires_at: session[:bitbucket_expires_at],
+ expires_in: session[:bitbucket_expires_in],
+ refresh_token: session[:bitbucket_refresh_token]
}
end
end
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 7e4da73bc11..c2e4d62b50b 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -12,7 +12,7 @@ class JwtController < ApplicationController
return head :not_found unless service
result = service.new(@authentication_result.project, @authentication_result.actor, auth_params).
- execute(authentication_abilities: @authentication_result.authentication_abilities || [])
+ execute(authentication_abilities: @authentication_result.authentication_abilities)
render json: result, status: result[:http_status]
end
@@ -20,13 +20,13 @@ class JwtController < ApplicationController
private
def authenticate_project_or_user
- @authentication_result = Gitlab::Auth::Result.new
+ @authentication_result = Gitlab::Auth::Result.new(nil, nil, :none, Gitlab::Auth.read_authentication_abilities)
authenticate_with_http_basic do |login, password|
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
render_unauthorized unless @authentication_result.success? &&
- (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
+ (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
end
rescue Gitlab::Auth::MissingPersonalTokenError
render_missing_personal_token
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index 0f54dfa4efc..2ae4785b12c 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -2,10 +2,12 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include Gitlab::CurrentSettings
include Gitlab::GonHelper
include PageLayoutHelper
+ include OauthApplications
before_action :verify_user_oauth_applications_enabled
before_action :authenticate_user!
before_action :add_gon_variables
+ before_action :load_scopes, only: [:index, :create, :edit]
layout 'profile'
diff --git a/app/controllers/profiles/avatars_controller.rb b/app/controllers/profiles/avatars_controller.rb
index f193adb46b4..daa51ae41df 100644
--- a/app/controllers/profiles/avatars_controller.rb
+++ b/app/controllers/profiles/avatars_controller.rb
@@ -4,7 +4,6 @@ class Profiles::AvatarsController < Profiles::ApplicationController
@user.remove_avatar!
@user.save
- @user.reset_events_cache
redirect_to profile_path
end
diff --git a/app/controllers/profiles/chat_names_controller.rb b/app/controllers/profiles/chat_names_controller.rb
new file mode 100644
index 00000000000..6a1f468ba5a
--- /dev/null
+++ b/app/controllers/profiles/chat_names_controller.rb
@@ -0,0 +1,64 @@
+class Profiles::ChatNamesController < Profiles::ApplicationController
+ before_action :chat_name_token, only: [:new]
+ before_action :chat_name_params, only: [:new, :create, :deny]
+
+ def index
+ @chat_names = current_user.chat_names
+ end
+
+ def new
+ end
+
+ def create
+ new_chat_name = current_user.chat_names.new(chat_name_params)
+
+ if new_chat_name.save
+ flash[:notice] = "Authorized #{new_chat_name.chat_name}"
+ else
+ flash[:alert] = "Could not authorize chat nickname. Try again!"
+ end
+
+ delete_chat_name_token
+ redirect_to profile_chat_names_path
+ end
+
+ def deny
+ delete_chat_name_token
+
+ flash[:notice] = "Denied authorization of chat nickname #{chat_name_params[:user_name]}."
+
+ redirect_to profile_chat_names_path
+ end
+
+ def destroy
+ @chat_name = chat_names.find(params[:id])
+
+ if @chat_name.destroy
+ flash[:notice] = "Deleted chat nickname: #{@chat_name.chat_name}!"
+ else
+ flash[:alert] = "Could not delete chat nickname #{@chat_name.chat_name}."
+ end
+
+ redirect_to profile_chat_names_path
+ end
+
+ private
+
+ def delete_chat_name_token
+ chat_name_token.delete
+ end
+
+ def chat_name_params
+ @chat_name_params ||= chat_name_token.get || render_404
+ end
+
+ def chat_name_token
+ return render_404 unless params[:token] || render_404
+
+ @chat_name_token ||= Gitlab::ChatNameToken.new(params[:token])
+ end
+
+ def chat_names
+ @chat_names ||= current_user.chat_names
+ end
+end
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 508b82a9a6c..6e007f17913 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -1,8 +1,6 @@
class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
- before_action :load_personal_access_tokens, only: :index
-
def index
- @personal_access_token = current_user.personal_access_tokens.build
+ set_index_vars
end
def create
@@ -12,7 +10,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
flash[:personal_access_token] = @personal_access_token.token
redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created."
else
- load_personal_access_tokens
+ set_index_vars
render :index
end
end
@@ -32,10 +30,12 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
private
def personal_access_token_params
- params.require(:personal_access_token).permit(:name, :expires_at)
+ params.require(:personal_access_token).permit(:name, :expires_at, scopes: [])
end
- def load_personal_access_tokens
+ def set_index_vars
+ @personal_access_token ||= current_user.personal_access_tokens.build
+ @scopes = Gitlab::Auth::SCOPES
@active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at)
@inactive_personal_access_tokens = current_user.personal_access_tokens.inactive
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 9eb75bb3891..18044ca78e2 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -22,6 +22,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
end
@qr_code = build_qr_code
+ @account_string = account_string
setup_u2f_registration
end
@@ -78,11 +79,14 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
private
def build_qr_code
- issuer = "#{issuer_host} | #{current_user.email}"
- uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer)
+ uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host)
RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3)
end
+ def account_string
+ "#{issuer_host}:#{current_user.email}"
+ end
+
def issuer_host
Gitlab.config.gitlab.host
end
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
new file mode 100644
index 00000000000..d9dfa534669
--- /dev/null
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -0,0 +1,48 @@
+class Projects::AutocompleteSourcesController < Projects::ApplicationController
+ before_action :load_autocomplete_service, except: [:emojis, :members]
+
+ def emojis
+ render json: Gitlab::AwardEmoji.urls
+ end
+
+ def members
+ render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
+ end
+
+ def issues
+ render json: @autocomplete_service.issues
+ end
+
+ def merge_requests
+ render json: @autocomplete_service.merge_requests
+ end
+
+ def labels
+ render json: @autocomplete_service.labels
+ end
+
+ def milestones
+ render json: @autocomplete_service.milestones
+ end
+
+ def commands
+ render json: @autocomplete_service.commands(noteable, params[:type])
+ end
+
+ private
+
+ def load_autocomplete_service
+ @autocomplete_service = ::Projects::AutocompleteService.new(@project, current_user)
+ end
+
+ def noteable
+ case params[:type]
+ when 'Issue'
+ IssuesFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
+ when 'MergeRequest'
+ MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
+ when 'Commit'
+ @project.commit(params[:type_id])
+ end
+ end
+end
diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb
index ada7db3c552..53788687076 100644
--- a/app/controllers/projects/avatars_controller.rb
+++ b/app/controllers/projects/avatars_controller.rb
@@ -20,7 +20,6 @@ class Projects::AvatarsController < Projects::ApplicationController
@project.remove_avatar!
@project.save
- @project.reset_events_cache
redirect_to edit_project_path(@project)
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index b78cc6585ba..9940263ae24 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -13,7 +13,6 @@ class Projects::BlobController < Projects::ApplicationController
before_action :assign_blob_vars
before_action :commit, except: [:new, :create]
before_action :blob, except: [:new, :create]
- before_action :from_merge_request, only: [:edit, :update]
before_action :require_branch_head, only: [:edit, :update]
before_action :editor_variables, except: [:show, :preview, :diff]
before_action :validate_diff_params, only: :diff
@@ -39,14 +38,6 @@ class Projects::BlobController < Projects::ApplicationController
def update
@path = params[:file_path] if params[:file_path].present?
- after_edit_path =
- if from_merge_request && @target_branch == @ref
- diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
- "#file-path-#{hexdigest(@path)}"
- else
- namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path))
- end
-
create_commit(Files::UpdateService, success_path: after_edit_path,
failure_view: :edit,
failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
@@ -124,9 +115,14 @@ class Projects::BlobController < Projects::ApplicationController
render_404
end
- def from_merge_request
- # If blob edit was initiated from merge request page
- @from_merge_request ||= MergeRequest.find_by(id: params[:from_merge_request_id])
+ def after_edit_path
+ from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid])
+ if from_merge_request && @target_branch == @ref
+ diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
+ "##{hexdigest(@path)}"
+ else
+ namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path))
+ end
end
def editor_variables
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 2de8ada3e29..89d84809e3a 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -4,7 +4,7 @@ class Projects::BranchesController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
- before_action :authorize_push_code!, only: [:new, :create, :destroy]
+ before_action :authorize_push_code!, only: [:new, :create, :destroy, :destroy_all_merged]
def index
@sort = params[:sort].presence || sort_value_name
@@ -36,7 +36,7 @@ class Projects::BranchesController < Projects::ApplicationController
execute(branch_name, ref)
if params[:issue_iid]
- issue = @project.issues.find_by(iid: params[:issue_iid])
+ issue = IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:issue_iid])
SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue
end
@@ -62,6 +62,13 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
+ def destroy_all_merged
+ DeleteMergedBranchesService.new(@project, current_user).async_execute
+
+ redirect_to namespace_project_branches_path(@project.namespace, @project),
+ notice: 'Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes.'
+ end
+
private
def ref
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index cdfc1ba7b92..8197d9e4c99 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -65,7 +65,7 @@ class Projects::CommitController < Projects::ApplicationController
return render_404 if @target_branch.blank?
- create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title} has been successfully reverted.",
+ create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.",
success_path: successful_change_path, failure_path: failed_change_path)
end
@@ -74,26 +74,24 @@ class Projects::CommitController < Projects::ApplicationController
return render_404 if @target_branch.blank?
- create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title} has been successfully cherry-picked.",
+ create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked.",
success_path: successful_change_path, failure_path: failed_change_path)
end
private
def successful_change_path
- return referenced_merge_request_url if @commit.merged_merge_request
-
- namespace_project_commits_url(@project.namespace, @project, @target_branch)
+ referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @target_branch)
end
def failed_change_path
- return referenced_merge_request_url if @commit.merged_merge_request
-
- namespace_project_commit_url(@project.namespace, @project, params[:id])
+ referenced_merge_request_url || namespace_project_commit_url(@project.namespace, @project, params[:id])
end
def referenced_merge_request_url
- namespace_project_merge_request_url(@project.namespace, @project, @commit.merged_merge_request)
+ if merge_request = @commit.merged_merge_request(current_user)
+ namespace_project_merge_request_url(@project.namespace, @project, merge_request)
+ end
end
def commit
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index aba87b6144b..ad92f05a42d 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -21,7 +21,7 @@ class Projects::CommitsController < Projects::ApplicationController
@note_counts = project.notes.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
- @merge_request = @project.merge_requests.opened.
+ @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.
find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref)
respond_to do |format|
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index bee3d56076c..ec02fc15d35 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -53,7 +53,7 @@ class Projects::CompareController < Projects::ApplicationController
end
def merge_request
- @merge_request ||= @project.merge_requests.opened.
+ @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.
find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref)
end
end
diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb
new file mode 100644
index 00000000000..13b3eec761f
--- /dev/null
+++ b/app/controllers/projects/cycle_analytics/events_controller.rb
@@ -0,0 +1,65 @@
+module Projects
+ module CycleAnalytics
+ class EventsController < Projects::ApplicationController
+ include CycleAnalyticsParams
+
+ before_action :authorize_read_cycle_analytics!
+ before_action :authorize_read_build!, only: [:test, :staging]
+ before_action :authorize_read_issue!, only: [:issue, :production]
+ before_action :authorize_read_merge_request!, only: [:code, :review]
+
+ def issue
+ render_events(events.issue_events)
+ end
+
+ def plan
+ render_events(events.plan_events)
+ end
+
+ def code
+ render_events(events.code_events)
+ end
+
+ def test
+ options[:branch] = events_params[:branch_name]
+
+ render_events(events.test_events)
+ end
+
+ def review
+ render_events(events.review_events)
+ end
+
+ def staging
+ render_events(events.staging_events)
+ end
+
+ def production
+ render_events(events.production_events)
+ end
+
+ private
+
+ def render_events(events_list)
+ respond_to do |format|
+ format.html
+ format.json { render json: { events: events_list } }
+ end
+ end
+
+ def events
+ @events ||= Gitlab::CycleAnalytics::Events.new(project: project, options: options)
+ end
+
+ def options
+ @options ||= { from: start_date(events_params), current_user: current_user }
+ end
+
+ def events_params
+ return {} unless params[:events].present?
+
+ params[:events].slice(:start_date, :branch_name)
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
index 16a7b1fc6e2..ac639ef015b 100644
--- a/app/controllers/projects/cycle_analytics_controller.rb
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -1,11 +1,16 @@
class Projects::CycleAnalyticsController < Projects::ApplicationController
include ActionView::Helpers::DateHelper
include ActionView::Helpers::TextHelper
+ include CycleAnalyticsParams
before_action :authorize_read_cycle_analytics!
def show
- @cycle_analytics = CycleAnalytics.new(@project, from: parse_start_date)
+ @cycle_analytics = ::CycleAnalytics.new(@project, current_user, from: start_date(cycle_analytics_params))
+
+ stats_values, cycle_analytics_json = generate_cycle_analytics_data
+
+ @cycle_analytics_no_data = stats_values.blank?
respond_to do |format|
format.html
@@ -15,37 +20,35 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
private
- def parse_start_date
- case cycle_analytics_params[:start_date]
- when '30' then 30.days.ago
- when '90' then 90.days.ago
- else 90.days.ago
- end
- end
-
def cycle_analytics_params
return {} unless params[:cycle_analytics].present?
{ start_date: params[:cycle_analytics][:start_date] }
end
- def cycle_analytics_json
- cycle_analytics_view_data = [[:issue, "Issue", "Time before an issue gets scheduled"],
- [:plan, "Plan", "Time before an issue starts implementation"],
- [:code, "Code", "Time until first merge request"],
- [:test, "Test", "Total test time for all commits/merges"],
- [:review, "Review", "Time between merge request creation and merge/close"],
- [:staging, "Staging", "From merge request merge until deploy to production"],
- [:production, "Production", "From issue creation until deploy to production"]]
+ def generate_cycle_analytics_data
+ stats_values = []
- stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_description)|
+ cycle_analytics_view_data = [[:issue, "Issue", "Related Issues", "Time before an issue gets scheduled"],
+ [:plan, "Plan", "Related Commits", "Time before an issue starts implementation"],
+ [:code, "Code", "Related Merge Requests", "Time spent coding"],
+ [:test, "Test", "Relative Builds Trigger by Commits", "The time taken to build and test the application"],
+ [:review, "Review", "Relative Merged Requests", "The time taken to review the code"],
+ [:staging, "Staging", "Relative Deployed Builds", "The time taken in staging"],
+ [:production, "Production", "Related Issues", "The total time taken from idea to production"]]
+
+ stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_legend, stage_description)|
value = @cycle_analytics.send(stage_method).presence
+ stats_values << value.abs if value
+
stats << {
title: stage_text,
description: stage_description,
+ legend: stage_legend,
value: value && !value.zero? ? distance_of_time_in_words(value) : nil
}
+
stats
end
@@ -59,9 +62,11 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
{ title: "Deploy".pluralize(deploys), value: deploys }
]
- {
- summary: summary,
- stats: stats
+ cycle_analytics_hash = { summary: summary,
+ stats: stats,
+ permissions: @cycle_analytics.permissions(user: current_user)
}
+
+ [stats_values, cycle_analytics_hash]
end
end
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
index d174e1145a7..1349b015a63 100644
--- a/app/controllers/projects/discussions_controller.rb
+++ b/app/controllers/projects/discussions_controller.rb
@@ -5,9 +5,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
before_action :authorize_resolve_discussion!
def resolve
- discussion.resolve!(current_user)
-
- MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request)
+ Discussions::ResolveService.new(project, current_user, merge_request: merge_request).execute(discussion)
render json: {
resolved_by: discussion.resolved_by.try(:name),
@@ -26,7 +24,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
private
def merge_request
- @merge_request ||= @project.merge_requests.find_by!(iid: params[:merge_request_id])
+ @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).find_by!(iid: params[:merge_request_id])
end
def discussion
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index ea22b2dcc15..6bd4cb3f2f5 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -8,13 +8,16 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def index
@scope = params[:scope]
- @all_environments = project.environments
- @environments =
- if @scope == 'stopped'
- @all_environments.stopped
- else
- @all_environments.available
+ @environments = project.environments
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: EnvironmentSerializer
+ .new(project: @project)
+ .represent(@environments)
end
+ end
end
def show
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index ade01c706a7..ba46e2528e6 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -4,6 +4,7 @@ class Projects::ForksController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
+ before_action :authenticate_user!, only: [:new, :create]
def index
base_query = project.forks.includes(:creator)
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 383e184d796..8714349e27f 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -18,13 +18,17 @@ class Projects::GitHttpClientController < Projects::ApplicationController
private
+ def download_request?
+ raise NotImplementedError
+ end
+
+ def upload_request?
+ raise NotImplementedError
+ end
+
def authenticate_user
@authentication_result = Gitlab::Auth::Result.new
- if project && project.public? && download_request?
- return # Allow access
- end
-
if allow_basic_auth? && basic_auth_provided?
login, password = user_name_and_password(request)
@@ -41,6 +45,10 @@ class Projects::GitHttpClientController < Projects::ApplicationController
send_final_spnego_response
return # Allow access
end
+ elsif project && download_request? && Guest.can?(:download_code, project)
+ @authentication_result = Gitlab::Auth::Result.new(nil, project, :none, [:download_code])
+
+ return # Allow access
end
send_challenges
@@ -130,10 +138,6 @@ class Projects::GitHttpClientController < Projects::ApplicationController
authentication_result.ci?(project)
end
- def lfs_deploy_token?
- authentication_result.lfs_deploy_token?(project)
- end
-
def authentication_has_download_access?
has_authentication_ability?(:download_code) || has_authentication_ability?(:build_download_code)
end
@@ -149,8 +153,4 @@ class Projects::GitHttpClientController < Projects::ApplicationController
def authentication_project
authentication_result.project
end
-
- def verify_workhorse_api!
- Gitlab::Workhorse.verify_api_request!(request.headers)
- end
end
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index 662d38b10a5..9184dcccac5 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -1,7 +1,5 @@
-# This file should be identical in GitLab Community Edition and Enterprise Edition
-
class Projects::GitHttpController < Projects::GitHttpClientController
- before_action :verify_workhorse_api!
+ include WorkhorseRequest
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
@@ -67,22 +65,22 @@ class Projects::GitHttpController < Projects::GitHttpClientController
end
def render_denied
- if user && user.can?(:read_project, project)
- render plain: 'Access denied', status: :forbidden
+ if user && can?(user, :read_project, project)
+ render plain: access_denied_message, status: :forbidden
else
# Do not leak information about project existence
render_not_found
end
end
+ def access_denied_message
+ 'Access denied'
+ end
+
def upload_pack_allowed?
return false unless Gitlab.config.gitlab_shell.upload_pack
- if user
- access_check.allowed?
- else
- ci? || project.public?
- end
+ access_check.allowed? || ci?
end
def access
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 3f1a1d1c511..4f66e01e0f7 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -46,8 +46,9 @@ class Projects::IssuesController < Projects::ApplicationController
params[:issue] ||= ActionController::Parameters.new(
assignee_id: ""
)
+ build_params = issue_params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
+ @issue = @noteable = Issues::BuildService.new(project, current_user, build_params).execute
- @issue = @noteable = @project.issues.new(issue_params)
respond_with(@issue)
end
@@ -69,13 +70,15 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to do |format|
format.html
format.json do
- render json: @issue.to_json(include: [:milestone, :labels])
+ render json: IssueSerializer.new.represent(@issue)
end
end
end
def create
- @issue = Issues::CreateService.new(project, current_user, issue_params.merge(request: request)).execute
+ extra_params = { request: request,
+ merge_request_for_resolving_discussions: merge_request_for_resolving_discussions }
+ @issue = Issues::CreateService.new(project, current_user, issue_params.merge(extra_params)).execute
respond_to do |format|
format.html do
@@ -169,6 +172,14 @@ class Projects::IssuesController < Projects::ApplicationController
alias_method :awardable, :issue
alias_method :spammable, :issue
+ def merge_request_for_resolving_discussions
+ return unless merge_request_iid = params[:merge_request_for_resolving_discussions]
+
+ @merge_request_for_resolving_discussions ||= MergeRequestsFinder.new(current_user, project_id: project.id).
+ execute.
+ find_by(iid: merge_request_iid)
+ end
+
def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue)
end
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 42fd09e9b7e..824ed7be73e 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -3,7 +3,7 @@ class Projects::LabelsController < Projects::ApplicationController
before_action :module_enabled
before_action :label, only: [:edit, :update, :destroy]
- before_action :find_labels, only: [:index, :set_priorities, :remove_priority]
+ before_action :find_labels, only: [:index, :set_priorities, :remove_priority, :toggle_subscription]
before_action :authorize_read_label!
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update,
:generate, :destroy, :remove_priority,
@@ -123,7 +123,10 @@ class Projects::LabelsController < Projects::ApplicationController
def label
@label ||= @project.labels.find(params[:id])
end
- alias_method :subscribable_resource, :label
+
+ def subscribable_resource
+ @available_labels.find(params[:id])
+ end
def find_labels
@available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index ece49dcd922..440259b643c 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -1,8 +1,7 @@
class Projects::LfsApiController < Projects::GitHttpClientController
- include LfsHelper
+ include LfsRequest
- before_action :require_lfs_enabled!
- before_action :lfs_check_access!, except: [:deprecated]
+ skip_before_action :lfs_check_access!, only: [:deprecated]
def batch
unless objects.present?
@@ -31,8 +30,12 @@ class Projects::LfsApiController < Projects::GitHttpClientController
private
- def objects
- @objects ||= (params[:objects] || []).to_a
+ def download_request?
+ params[:operation] == 'download'
+ end
+
+ def upload_request?
+ params[:operation] == 'upload'
end
def existing_oids
@@ -83,12 +86,4 @@ class Projects::LfsApiController < Projects::GitHttpClientController
}
}
end
-
- def download_request?
- params[:operation] == 'download'
- end
-
- def upload_request?
- params[:operation] == 'upload'
- end
end
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index 9005b104e90..32759672b6c 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -1,9 +1,8 @@
class Projects::LfsStorageController < Projects::GitHttpClientController
- include LfsHelper
+ include LfsRequest
+ include WorkhorseRequest
- before_action :require_lfs_enabled!
- before_action :lfs_check_access!
- before_action :verify_workhorse_api!, only: [:upload_authorize]
+ skip_before_action :verify_workhorse_api!, only: [:download, :upload_finalize]
def download
lfs_object = LfsObject.find_by_oid(oid)
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 9f104d903cc..f0cb5a9d4b4 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -38,7 +38,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def index
@merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page])
- @merge_requests = @merge_requests.preload(:target_project)
if params[:label_name].present?
labels_params = { project_id: @project.id, title: params[:label_name] }
@@ -61,7 +60,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
format.html { define_discussion_vars }
format.json do
- render json: @merge_request
+ render json: MergeRequestSerializer.new.represent(@merge_request)
end
format.patch do
@@ -83,12 +82,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request_diff =
if params[:diff_id]
- @merge_request.merge_request_diffs.find(params[:diff_id])
+ @merge_request.merge_request_diffs.viewable.find(params[:diff_id])
else
@merge_request.merge_request_diff
end
- @merge_request_diffs = @merge_request.merge_request_diffs.select_without_diff
+ @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present?
@@ -303,9 +302,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def cancel_merge_when_build_succeeds
- return access_denied! unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user)
+ unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user)
+ return access_denied!
+ end
- MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user).cancel(@merge_request)
+ MergeRequests::MergeWhenPipelineSucceedsService
+ .new(@project, current_user)
+ .cancel(@merge_request)
end
def merge
@@ -326,16 +329,18 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.update(merge_error: nil)
if params[:merge_when_build_succeeds].present?
- unless @merge_request.pipeline
+ unless @merge_request.head_pipeline
@status = :failed
return
end
- if @merge_request.pipeline.active?
- MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
- .execute(@merge_request)
+ if @merge_request.head_pipeline.active?
+ MergeRequests::MergeWhenPipelineSucceedsService
+ .new(@project, current_user, merge_params)
+ .execute(@merge_request)
+
@status = :merge_when_build_succeeds
- elsif @merge_request.pipeline.success?
+ elsif @merge_request.head_pipeline.success?
# This can be triggered when a user clicks the auto merge button while
# the tests finish at about the same time
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@@ -399,7 +404,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def ci_status
- pipeline = @merge_request.pipeline
+ pipeline = @merge_request.head_pipeline
+
if pipeline
status = pipeline.status
coverage = pipeline.try(:coverage)
@@ -418,7 +424,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
response = {
title: merge_request.title,
- sha: merge_request.diff_head_commit.short_id,
+ sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha),
status: status,
coverage: coverage
}
@@ -492,7 +498,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def validates_merge_request
# Show git not found page
# if there is no saved commits between source & target branch
- if @merge_request.commits.blank?
+ if @merge_request.has_no_commits?
# and if target branch doesn't exist
return invalid_mr unless @merge_request.target_branch_exists?
end
@@ -500,13 +506,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_show_vars
@noteable = @merge_request
- @commits_count = @merge_request.commits.count
+ @commits_count = @merge_request.commits_count
if @merge_request.locked_long_ago?
@merge_request.unlock_mr
@merge_request.close
end
+ labels
define_pipelines_vars
end
@@ -534,7 +541,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def define_widget_vars
- @pipeline = @merge_request.pipeline
+ @pipeline = @merge_request.head_pipeline
end
def define_commit_vars
@@ -563,11 +570,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_pipelines_vars
@pipelines = @merge_request.all_pipelines
-
- if @pipelines.present?
- @pipeline = @pipelines.first
- @statuses = @pipeline.statuses.relevant
- end
+ @pipeline = @merge_request.head_pipeline
+ @statuses_count = @pipeline.present? ? @pipeline.statuses.relevant.count : 0
end
def define_new_vars
@@ -634,7 +638,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def merge_when_build_succeeds_active?
params[:merge_when_build_succeeds].present? &&
- @merge_request.pipeline && @merge_request.pipeline.active?
+ @merge_request.head_pipeline && @merge_request.head_pipeline.active?
end
def build_merge_request
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index ff63f22cb5b..be52b0fa7cf 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -112,6 +112,6 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def milestone_params
- params.require(:milestone).permit(:title, :description, :due_date, :state_event)
+ params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
end
end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 0948ad21649..b71509f2c9b 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -146,24 +146,26 @@ class Projects::NotesController < Projects::ApplicationController
end
def note_json(note)
+ attrs = {
+ award: false,
+ id: note.id
+ }
+
if note.is_a?(AwardEmoji)
- {
+ attrs.merge!(
valid: note.valid?,
award: true,
- id: note.id,
name: note.name
- }
+ )
elsif note.persisted?
Banzai::NoteRenderer.render([note], @project, current_user)
- attrs = {
+ attrs.merge!(
valid: true,
- id: note.id,
discussion_id: note.discussion_id,
html: note_html(note),
- award: false,
note: note.note
- }
+ )
if note.diff_note?
discussion = note.to_discussion
@@ -188,15 +190,15 @@ class Projects::NotesController < Projects::ApplicationController
attrs[:original_discussion_id] = note.original_discussion_id
end
end
-
- attrs
else
- {
+ attrs.merge!(
valid: false,
- award: false,
errors: note.errors
- }
+ )
end
+
+ attrs[:commands_changes] = note.commands_changes unless attrs[:award]
+ attrs
end
def authorize_admin_note!
@@ -215,6 +217,6 @@ class Projects::NotesController < Projects::ApplicationController
end
def find_current_user_notes
- @notes = NotesFinder.new.execute(project, current_user, params)
+ @notes = NotesFinder.new(project, current_user, params).execute.inc_author
end
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 371cc3787fb..85188cfdd4c 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -1,6 +1,6 @@
class Projects::PipelinesController < Projects::ApplicationController
before_action :pipeline, except: [:index, :new, :create]
- before_action :commit, only: [:show]
+ before_action :commit, only: [:show, :builds]
before_action :authorize_read_pipeline!
before_action :authorize_create_pipeline!, only: [:new, :create]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
@@ -18,7 +18,9 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def create
- @pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute(ignore_skip_ci: true, save_on_errors: false)
+ @pipeline = Ci::CreatePipelineService
+ .new(project, current_user, create_params)
+ .execute(ignore_skip_ci: true, save_on_errors: false)
unless @pipeline.persisted?
render 'new'
return
@@ -30,6 +32,14 @@ class Projects::PipelinesController < Projects::ApplicationController
def show
end
+ def builds
+ respond_to do |format|
+ format.html do
+ render 'show'
+ end
+ end
+ end
+
def retry
pipeline.retry_failed(current_user)
diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb
index 9136633b87a..53ce23221ed 100644
--- a/app/controllers/projects/pipelines_settings_controller.rb
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -17,7 +17,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
flash[:notice] = "CI/CD Pipelines settings for '#{@project.name}' were successfully updated."
redirect_to namespace_project_pipelines_settings_path(@project.namespace, @project)
else
- render 'index'
+ render 'show'
end
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 699a56ae2f8..3aec6f18e27 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -1,23 +1,49 @@
class Projects::ProjectMembersController < Projects::ApplicationController
include MembershipActions
+ include SortingHelper
# Authorize
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index
+ @sort = params[:sort].presence || sort_value_name
@group_links = @project.project_group_links
@project_members = @project.project_members
@project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
+ group = @project.group
+
+ if group
+ # We need `.where.not(user_id: nil)` here otherwise when a group has an
+ # invitee, it would make the following query return 0 rows since a NULL
+ # user_id would be present in the subquery
+ # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
+ # FIXME: This whole logic should be moved to a finder!
+ non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id)
+ group_members = group.group_members.where.not(user_id: non_null_user_ids)
+ group_members = group_members.non_invite unless can?(current_user, :admin_group, @group)
+ end
+
if params[:search].present?
- users = @project.users.search(params[:search]).to_a
- @project_members = @project_members.where(user_id: users)
+ user_ids = @project.users.search(params[:search]).select(:id)
+ @project_members = @project_members.where(user_id: user_ids)
+
+ if group_members
+ user_ids = group.users.search(params[:search]).select(:id)
+ group_members = group_members.where(user_id: user_ids)
+ end
@group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end
- @project_members = @project_members.order(access_level: :desc).page(params[:page])
+ wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"]
+ wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members
+
+ @project_members = Member.
+ where(wheres.join(' OR ')).
+ sort(@sort).
+ page(params[:page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user)
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index 0825a4311cb..2c097cb4d8d 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -10,7 +10,14 @@ class Projects::ReleasesController < Projects::ApplicationController
end
def update
- release.update_attributes(release_params)
+ # Release belongs to Tag which is not active record object,
+ # it exists only to save a description to each Tag.
+ # If description is empty we should destroy the existing record.
+ if release_params[:description].present?
+ release.update_attributes(release_params)
+ else
+ release.destroy
+ end
redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name)
end
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 97e6e9471e0..30c2a5d9982 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -10,8 +10,7 @@ class Projects::ServicesController < Projects::ApplicationController
layout "project_settings"
def index
- @project.build_missing_services
- @services = @project.services.visible.reload
+ @services = @project.find_or_initialize_services
end
def edit
@@ -29,6 +28,8 @@ class Projects::ServicesController < Projects::ApplicationController
end
def test
+ return render_404 unless @service.can_test?
+
data = @service.test_data(project, current_user)
outcome = @service.test(data)
@@ -46,6 +47,6 @@ class Projects::ServicesController < Projects::ApplicationController
private
def service
- @service ||= @project.services.find { |service| service.to_param == params[:id] }
+ @service ||= @project.find_or_initialize_service(params[:id])
end
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index e290a0eadda..0720be2e55d 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -19,10 +19,12 @@ class Projects::SnippetsController < Projects::ApplicationController
respond_to :html
def index
- @snippets = SnippetsFinder.new.execute(current_user, {
+ @snippets = SnippetsFinder.new.execute(
+ current_user,
filter: :by_project,
- project: @project
- })
+ project: @project,
+ scope: params[:scope]
+ )
@snippets = @snippets.page(params[:page])
end
diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb
index 5685d0f4e7c..a41fcb85c40 100644
--- a/app/controllers/projects/todos_controller.rb
+++ b/app/controllers/projects/todos_controller.rb
@@ -16,15 +16,9 @@ class Projects::TodosController < Projects::ApplicationController
@issuable ||= begin
case params[:issuable_type]
when "issue"
- issue = @project.issues.find(params[:issuable_id])
-
- if can?(current_user, :read_issue, issue)
- issue
- else
- render_404
- end
+ IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id])
when "merge_request"
- @project.merge_requests.find(params[:issuable_id])
+ MergeRequestsFinder.new(current_user, project_id: @project.id).find(params[:issuable_id])
end
end
end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 177ccf5eec9..c3353446fd1 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -115,6 +115,8 @@ class Projects::WikisController < Projects::ApplicationController
# Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki
+
+ @sidebar_wiki_pages = @project_wiki.pages.first(15)
rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
redirect_to project_path(@project)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 28820adcc46..d5ee503c44c 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -127,39 +127,6 @@ class ProjectsController < Projects::ApplicationController
redirect_to edit_project_path(@project), alert: ex.message
end
- def autocomplete_sources
- noteable =
- case params[:type]
- when 'Issue'
- IssuesFinder.new(current_user, project_id: @project.id).
- execute.find_by(iid: params[:type_id])
- when 'MergeRequest'
- MergeRequestsFinder.new(current_user, project_id: @project.id).
- execute.find_by(iid: params[:type_id])
- when 'Commit'
- @project.commit(params[:type_id])
- else
- nil
- end
-
- autocomplete = ::Projects::AutocompleteService.new(@project, current_user)
- participants = ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
-
- @suggestions = {
- emojis: Gitlab::AwardEmoji.urls,
- issues: autocomplete.issues,
- milestones: autocomplete.milestones,
- mergerequests: autocomplete.merge_requests,
- labels: autocomplete.labels,
- members: participants,
- commands: autocomplete.commands(noteable, params[:type])
- }
-
- respond_to do |format|
- format.json { render json: @suggestions }
- end
- end
-
def new_issue_address
return render_404 unless Gitlab::IncomingEmail.supports_issue_creation?
@@ -325,26 +292,44 @@ class ProjectsController < Projects::ApplicationController
end
def project_params
- project_feature_attributes =
- {
- project_feature_attributes:
- [
- :issues_access_level, :builds_access_level,
- :wiki_access_level, :merge_requests_access_level,
- :snippets_access_level, :repository_access_level
- ]
- }
+ params.require(:project)
+ .permit(project_params_ce)
+ end
- params.require(:project).permit(
- :name, :path, :description, :issues_tracker, :tag_list, :runners_token,
+ def project_params_ce
+ [
+ :avatar,
+ :build_allow_git_fetch,
+ :build_coverage_regex,
+ :build_timeout_in_minutes,
:container_registry_enabled,
- :issues_tracker_id, :default_branch,
- :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
- :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
- :public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled,
+ :default_branch,
+ :description,
+ :import_url,
+ :issues_tracker,
+ :issues_tracker_id,
+ :last_activity_at,
+ :lfs_enabled,
+ :name,
+ :namespace_id,
:only_allow_merge_if_all_discussions_are_resolved,
- :lfs_enabled, project_feature_attributes
- )
+ :only_allow_merge_if_build_succeeds,
+ :path,
+ :public_builds,
+ :request_access_enabled,
+ :runners_token,
+ :tag_list,
+ :visibility_level,
+
+ project_feature_attributes: %i[
+ builds_access_level
+ issues_access_level
+ merge_requests_access_level
+ repository_access_level
+ snippets_access_level
+ wiki_access_level
+ ]
+ ]
end
def repo_exists?
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 3327f4f2b87..c45196cc3e9 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -27,7 +27,10 @@ class RegistrationsController < Devise::RegistrationsController
DeleteUserService.new(current_user).execute(current_user)
respond_to do |format|
- format.html { redirect_to new_user_session_path, notice: "Account successfully removed." }
+ format.html do
+ session.try(:destroy)
+ redirect_to new_user_session_path, notice: "Account successfully removed."
+ end
end
end
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index d01e0dedf52..b666aa01d6b 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -16,7 +16,7 @@ class SearchController < ApplicationController
@group = nil unless can?(current_user, :read_group, @group)
end
- return if params[:search].nil? || params[:search].blank?
+ return if params[:search].blank?
@search_term = params[:search]
diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb
index 3085ff33aba..04c36b3ebfe 100644
--- a/app/controllers/sent_notifications_controller.rb
+++ b/app/controllers/sent_notifications_controller.rb
@@ -12,7 +12,7 @@ class SentNotificationsController < ApplicationController
def unsubscribe_and_redirect
noteable = @sent_notification.noteable
- noteable.unsubscribe(@sent_notification.recipient)
+ noteable.unsubscribe(@sent_notification.recipient, @sent_notification.project)
flash[:notice] = "You have been unsubscribed from this thread."
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 5d7ecfeacf4..93a180b9036 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -31,10 +31,18 @@ class SessionsController < Devise::SessionsController
resource.update_attributes(reset_password_token: nil,
reset_password_sent_at: nil)
end
+ # hide the signed-in notification
+ flash[:notice] = nil
log_audit_event(current_user, with: authentication_method)
end
end
+ def destroy
+ super
+ # hide the signed_out notice
+ flash[:notice] = nil
+ end
+
private
# Handle an "initial setup" state, where there's only one user, it's an admin,
@@ -106,7 +114,7 @@ class SessionsController < Devise::SessionsController
def valid_otp_attempt?(user)
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
- user.invalidate_otp_backup_code!(user_params[:otp_attempt])
+ user.invalidate_otp_backup_code!(user_params[:otp_attempt])
end
def log_audit_event(user, options = {})
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 6a881b271d7..6e29f1e8a65 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -86,7 +86,7 @@ class UsersController < ApplicationController
end
def exists
- render json: { exists: Namespace.where(path: params[:username].downcase).any? }
+ render json: { exists: !!Namespace.find_by_path_or_name(params[:username]) }
end
private
@@ -104,8 +104,7 @@ class UsersController < ApplicationController
end
def contributions_calendar
- @contributions_calendar ||= Gitlab::ContributionsCalendar.
- new(contributed_projects, user)
+ @contributions_calendar ||= Gitlab::ContributionsCalendar.new(user, current_user)
end
def load_events
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index cc2073081b5..b4c14d05eaf 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -7,7 +7,7 @@
# current_user - which user use
# params:
# scope: 'created-by-me' or 'assigned-to-me' or 'all'
-# state: 'open' or 'closed' or 'all'
+# state: 'opened' or 'closed' or 'all'
# group_id: integer
# project_id: integer
# milestone_title: string
@@ -15,15 +15,14 @@
# search: string
# label_name: string
# sort: string
+# non_archived: boolean
#
-require_relative 'projects_finder'
-
class IssuableFinder
NONE = '0'
attr_accessor :current_user, :params
- def initialize(current_user, params)
+ def initialize(current_user, params = {})
@current_user = current_user
@params = params
end
@@ -40,9 +39,48 @@ class IssuableFinder
items = by_author(items)
items = by_label(items)
items = by_due_date(items)
+ items = by_non_archived(items)
sort(items)
end
+ def find(*params)
+ execute.find(*params)
+ end
+
+ def find_by(*params)
+ execute.find_by(*params)
+ end
+
+ # We often get counts for each state by running a query per state, and
+ # counting those results. This is typically slower than running one query
+ # (even if that query is slower than any of the individual state queries) and
+ # grouping and counting within that query.
+ #
+ def count_by_state
+ count_params = params.merge(state: nil, sort: nil)
+ labels_count = label_names.any? ? label_names.count : 1
+ finder = self.class.new(current_user, count_params)
+ counts = Hash.new(0)
+
+ # Searching by label includes a GROUP BY in the query, but ours will be last
+ # because it is added last. Searching by multiple labels also includes a row
+ # per issuable, so we have to count those in Ruby - which is bad, but still
+ # better than performing multiple queries.
+ #
+ finder.execute.reorder(nil).group(:state).count.each do |key, value|
+ counts[Array(key).last.to_sym] += value / labels_count
+ end
+
+ counts[:all] = counts.values.sum
+ counts[:opened] += counts[:reopened]
+
+ counts
+ end
+
+ def find_by!(*params)
+ execute.find_by!(*params)
+ end
+
def group
return @group if defined?(@group)
@@ -61,31 +99,26 @@ class IssuableFinder
def project
return @project if defined?(@project)
- if project?
- @project = Project.find(params[:project_id])
+ project = Project.find(params[:project_id])
+ project = nil unless Ability.allowed?(current_user, :"read_#{klass.to_ability_name}", project)
- unless Ability.allowed?(current_user, :read_project, @project)
- @project = nil
- end
- else
- @project = nil
- end
-
- @project
+ @project = project
end
def projects
return @projects if defined?(@projects)
+ return @projects = project if project?
- if project?
- @projects = project
- elsif current_user && params[:authorized_only].presence && !current_user_related?
- @projects = current_user.authorized_projects.reorder(nil)
- elsif group
- @projects = GroupProjectsFinder.new(group).execute(current_user).reorder(nil)
- else
- @projects = ProjectsFinder.new.execute(current_user).reorder(nil)
- end
+ projects =
+ if current_user && params[:authorized_only].presence && !current_user_related?
+ current_user.authorized_projects
+ elsif group
+ GroupProjectsFinder.new(group).execute(current_user)
+ else
+ ProjectsFinder.new.execute(current_user)
+ end
+
+ @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
end
def search
@@ -180,10 +213,13 @@ class IssuableFinder
end
def by_state(items)
- params[:state] ||= 'all'
-
- if items.respond_to?(params[:state])
- items.public_send(params[:state])
+ case params[:state].to_s
+ when 'closed'
+ items.closed
+ when 'merged'
+ items.respond_to?(:merged) ? items.merged : items.closed
+ when 'opened'
+ items.opened
else
items
end
@@ -326,6 +362,10 @@ class IssuableFinder
end
end
+ def by_non_archived(items)
+ params[:non_archived].present? ? items.non_archived : items
+ end
+
def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
end
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index be00a219205..707eddd4d29 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -23,10 +23,26 @@ class IssuesFinder < IssuableFinder
private
def init_collection
- Issue.visible_to_user(current_user)
+ IssuesFinder.not_restricted_by_confidentiality(current_user)
end
def iid_pattern
@iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z}
end
+
+ def self.not_restricted_by_confidentiality(user)
+ return Issue.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
+
+ return Issue.all if user.admin?
+
+ Issue.where('
+ issues.confidential IS NULL
+ OR issues.confidential IS FALSE
+ OR (issues.confidential = TRUE
+ AND (issues.author_id = :user_id
+ OR issues.assignee_id = :user_id
+ OR issues.project_id IN(:project_ids)))',
+ user_id: user.id,
+ project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
+ end
end
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index 865f093f04a..fa0e2a5e3d8 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -6,7 +6,7 @@ class LabelsFinder < UnionFinder
def execute(skip_authorization: false)
@skip_authorization = skip_authorization
- items = find_union(label_ids, Label)
+ items = find_union(label_ids, Label) || Label.none
items = with_title(items)
sort(items)
end
@@ -18,9 +18,11 @@ class LabelsFinder < UnionFinder
def label_ids
label_ids = []
- if project
- label_ids << project.group.labels if project.group.present?
- label_ids << project.labels
+ if project?
+ if project
+ label_ids << project.group.labels if project.group.present?
+ label_ids << project.labels
+ end
else
label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id))
@@ -40,16 +42,16 @@ class LabelsFinder < UnionFinder
items.where(title: title)
end
- def group_id
- params[:group_id].presence
+ def group?
+ params[:group_id].present?
end
- def project_id
- params[:project_id].presence
+ def project?
+ params[:project_id].present?
end
- def projects_ids
- params[:project_ids]
+ def projects?
+ params[:project_ids].present?
end
def title
@@ -59,8 +61,9 @@ class LabelsFinder < UnionFinder
def project
return @project if defined?(@project)
- if project_id
- @project = find_project
+ if project?
+ @project = Project.find(params[:project_id])
+ @project = nil unless authorized_to_read_labels?(@project)
else
@project = nil
end
@@ -68,26 +71,20 @@ class LabelsFinder < UnionFinder
@project
end
- def find_project
- if skip_authorization
- Project.find_by(id: project_id)
- else
- available_projects.find_by(id: project_id)
- end
- end
-
def projects
return @projects if defined?(@projects)
- @projects = skip_authorization ? Project.all : available_projects
- @projects = @projects.in_namespace(group_id) if group_id
- @projects = @projects.where(id: projects_ids) if projects_ids
+ @projects = skip_authorization ? Project.all : ProjectsFinder.new.execute(current_user)
+ @projects = @projects.in_namespace(params[:group_id]) if group?
+ @projects = @projects.where(id: params[:project_ids]) if projects?
@projects = @projects.reorder(nil)
@projects
end
- def available_projects
- @available_projects ||= ProjectsFinder.new.execute(current_user)
+ def authorized_to_read_labels?(project)
+ return true if skip_authorization
+
+ Ability.allowed?(current_user, :read_label, project)
end
end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 3b254e7d9d5..8b82255445e 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -14,6 +14,7 @@
# search: string
# label_name: string
# sort: string
+# non_archived: boolean
#
class MergeRequestsFinder < IssuableFinder
def klass
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 0b7832e6583..4bd8c83081a 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -1,27 +1,102 @@
class NotesFinder
FETCH_OVERLAP = 5.seconds
- def execute(project, current_user, params)
- target_type = params[:target_type]
- target_id = params[:target_id]
- # Default to 0 to remain compatible with old clients
- last_fetched_at = Time.at(params.fetch(:last_fetched_at, 0).to_i)
-
- notes =
- case target_type
- when "commit"
- project.notes.for_commit_id(target_id).non_diff_notes
- when "issue"
- project.issues.visible_to_user(current_user).find(target_id).notes.inc_author
- when "merge_request"
- project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
- when "snippet", "project_snippet"
- project.snippets.find(target_id).notes
+ # Used to filter Notes
+ # When used with target_type and target_id this returns notes specifically for the controller
+ #
+ # Arguments:
+ # current_user - which user check authorizations with
+ # project - which project to look for notes on
+ # params:
+ # target_type: string
+ # target_id: integer
+ # last_fetched_at: time
+ # search: string
+ #
+ def initialize(project, current_user, params = {})
+ @project = project
+ @current_user = current_user
+ @params = params
+ init_collection
+ end
+
+ def execute
+ @notes = since_fetch_at(@params[:last_fetched_at]) if @params[:last_fetched_at]
+ @notes
+ end
+
+ private
+
+ def init_collection
+ if @params[:target_id]
+ @notes = on_target(@params[:target_type], @params[:target_id])
+ else
+ @notes = notes_of_any_type
+ end
+ end
+
+ def notes_of_any_type
+ types = %w(commit issue merge_request snippet)
+ note_relations = types.map { |t| notes_for_type(t) }
+ note_relations.map!{ |notes| search(@params[:search], notes) } if @params[:search]
+ UnionFinder.new.find_union(note_relations, Note)
+ end
+
+ def noteables_for_type(noteable_type)
+ case noteable_type
+ when "issue"
+ IssuesFinder.new(@current_user, project_id: @project.id).execute
+ when "merge_request"
+ MergeRequestsFinder.new(@current_user, project_id: @project.id).execute
+ when "snippet", "project_snippet"
+ SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project)
+ else
+ raise 'invalid target_type'
+ end
+ end
+
+ def notes_for_type(noteable_type)
+ if noteable_type == "commit"
+ if Ability.allowed?(@current_user, :download_code, @project)
+ @project.notes.where(noteable_type: 'Commit')
+ else
+ Note.none
+ end
+ else
+ finder = noteables_for_type(noteable_type)
+ @project.notes.where(noteable_type: finder.base_class.name, noteable_id: finder.reorder(nil))
+ end
+ end
+
+ def on_target(target_type, target_id)
+ if target_type == "commit"
+ notes_for_type('commit').for_commit_id(target_id)
+ else
+ target = noteables_for_type(target_type).find(target_id)
+
+ if target.respond_to?(:related_notes)
+ target.related_notes
else
- raise 'invalid target_type'
+ target.notes
end
+ end
+ end
+
+ # Searches for notes matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ def search(query, notes_relation = @notes)
+ pattern = "%#{query}%"
+ notes_relation.where(Note.arel_table[:note].matches(pattern))
+ end
+
+ # Notes changed since last fetch
+ # Uses overlapping intervals to avoid worrying about race conditions
+ def since_fetch_at(fetch_time)
+ # Default to 0 to remain compatible with old clients
+ last_fetched_at = Time.at(@params.fetch(:last_fetched_at, 0).to_i)
- # Use overlapping intervals to avoid worrying about race conditions
- notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh
+ @notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh
end
end
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index 00ff1611039..da6e6e87a6f 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -1,14 +1,17 @@
class SnippetsFinder
def execute(current_user, params = {})
filter = params[:filter]
+ user = params.fetch(:user, current_user)
case filter
when :all then
snippets(current_user).fresh
+ when :public then
+ Snippet.are_public.fresh
when :by_user then
- by_user(current_user, params[:user], params[:scope])
+ by_user(current_user, user, params[:scope])
when :by_project
- by_project(current_user, params[:project])
+ by_project(current_user, params[:project], params[:scope])
end
end
@@ -29,35 +32,35 @@ class SnippetsFinder
def by_user(current_user, user, scope)
snippets = user.snippets.fresh
- return snippets.are_public unless current_user
-
- if user == current_user
- case scope
- when 'are_internal' then
- snippets.are_internal
- when 'are_private' then
- snippets.are_private
- when 'are_public' then
- snippets.are_public
- else
- snippets
- end
+ if current_user
+ include_private = user == current_user
+ by_scope(snippets, scope, include_private)
else
- snippets.public_and_internal
+ snippets.are_public
end
end
- def by_project(current_user, project)
+ def by_project(current_user, project, scope)
snippets = project.snippets.fresh
if current_user
- if project.team.member?(current_user) || current_user.admin?
- snippets
- else
- snippets.public_and_internal
- end
+ include_private = project.team.member?(current_user) || current_user.admin?
+ by_scope(snippets, scope, include_private)
else
snippets.are_public
end
end
+
+ def by_scope(snippets, scope = nil, include_private = false)
+ case scope.to_s
+ when 'are_private'
+ include_private ? snippets.are_private : Snippet.none
+ when 'are_internal'
+ snippets.are_internal
+ when 'are_public'
+ snippets.are_public
+ else
+ include_private ? snippets : snippets.public_and_internal
+ end
+ end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index ebd78bf9888..c816b616631 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -151,7 +151,6 @@ module ApplicationHelper
# time - Time object
# placement - Tooltip placement String (default: "top")
# html_class - Custom class for `time` element (default: "time_ago")
- # skip_js - When true, exclude the `script` tag (default: false)
#
# By default also includes a `script` element with Javascript necessary to
# initialize the `timeago` jQuery extension. If this method is called many
@@ -163,22 +162,19 @@ module ApplicationHelper
# `html_class` argument is provided.
#
# Returns an HTML-safe String
- def time_ago_with_tooltip(time, placement: 'top', html_class: '', skip_js: false, short_format: false)
+ def time_ago_with_tooltip(time, placement: 'top', html_class: '', short_format: false)
css_classes = short_format ? 'js-short-timeago' : 'js-timeago'
css_classes << " #{html_class}" unless html_class.blank?
- css_classes << ' js-timeago-pending' unless skip_js
element = content_tag :time, time.to_s,
class: css_classes,
- datetime: time.to_time.getutc.iso8601,
title: time.to_time.in_time_zone.to_s(:medium),
- data: { toggle: 'tooltip', placement: placement, container: 'body' }
-
- unless skip_js
- element << javascript_tag(
- "$('.js-timeago-pending').removeClass('js-timeago-pending').timeago()"
- )
- end
+ datetime: time.to_time.getutc.iso8601,
+ data: {
+ toggle: 'tooltip',
+ placement: placement,
+ container: 'body'
+ }
element
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 45a567a1eba..60485160495 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -50,14 +50,14 @@ module ApplicationSettingsHelper
def restricted_level_checkboxes(help_block_id)
Gitlab::VisibilityLevel.options.map do |name, level|
checked = restricted_visibility_levels(true).include?(level)
- css_class = 'btn'
- css_class += ' active' if checked
- checkbox_name = 'application_setting[restricted_visibility_levels][]'
+ css_class = checked ? 'active' : ''
+ checkbox_name = "application_setting[restricted_visibility_levels][]"
- label_tag(checkbox_name, class: css_class) do
+ label_tag(name, class: css_class) do
check_box_tag(checkbox_name, level, checked,
autocomplete: 'off',
- 'aria-describedby' => help_block_id) + name
+ 'aria-describedby' => help_block_id,
+ id: name) + visibility_level_icon(level) + name
end
end
end
@@ -67,14 +67,14 @@ module ApplicationSettingsHelper
def import_sources_checkboxes(help_block_id)
Gitlab::ImportSources.options.map do |name, source|
checked = current_application_settings.import_sources.include?(source)
- css_class = 'btn'
- css_class += ' active' if checked
+ css_class = checked ? 'active' : ''
checkbox_name = 'application_setting[import_sources][]'
- label_tag(checkbox_name, class: css_class) do
+ label_tag(name, class: css_class) do
check_box_tag(checkbox_name, source, checked,
autocomplete: 'off',
- 'aria-describedby' => help_block_id) + name
+ 'aria-describedby' => help_block_id,
+ id: name.tr(' ', '_')) + name
end
end
end
@@ -100,4 +100,8 @@ module ApplicationSettingsHelper
options_for_select(options, @application_setting.repository_storages)
end
+
+ def sidekiq_queue_options_for_select
+ options_for_select(Sidekiq::Queue.all.map(&:name), @application_setting.sidekiq_throttling_queues)
+ end
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index cd4d778e508..92bac149313 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -3,7 +3,7 @@ module AuthHelper
FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze
def ldap_enabled?
- Gitlab.config.ldap.enabled
+ Gitlab::LDAP::Config.enabled?
end
def omniauth_enabled?
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
index f3aaff9140d..9fc69e12266 100644
--- a/app/helpers/builds_helper.rb
+++ b/app/helpers/builds_helper.rb
@@ -5,4 +5,14 @@ module BuildsHelper
build_class += ' retried' if build.retried?
build_class
end
+
+ def javascript_build_options
+ {
+ page_url: namespace_project_build_url(@project.namespace, @project, @build),
+ build_url: namespace_project_build_url(@project.namespace, @project, @build, :json),
+ build_status: @build.status,
+ build_stage: @build.stage,
+ log_state: @build.trace_with_state[:state].to_s
+ }
+ end
end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index dee3c78df47..4c7c16d694c 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -16,7 +16,7 @@ module ButtonHelper
# See http://clipboardjs.com/#usage
def clipboard_button(data = {})
css_class = data[:class] || 'btn-clipboard btn-transparent'
- title = data[:title] || 'Copy to Clipboard'
+ title = data[:title] || 'Copy to clipboard'
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
content_tag :button,
icon('clipboard'),
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 895c3d728ad..d9f5e01f0dc 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -4,17 +4,12 @@ module CiStatusHelper
builds_namespace_project_commit_path(project.namespace, project, pipeline.sha)
end
- def ci_status_with_icon(status, target = nil)
- content = ci_icon_for_status(status) + '&nbsp;'.html_safe + ci_label_for_status(status)
- klass = "ci-status ci-#{status}"
- if target
- link_to content, target, class: klass
- else
- content_tag :span, content, class: klass
+ # Is used by Commit and Merge Request Widget
+ def ci_label_for_status(status)
+ if detailed_status?(status)
+ return status.label
end
- end
- def ci_label_for_status(status)
case status
when 'success'
'passed'
@@ -31,6 +26,10 @@ module CiStatusHelper
end
def ci_icon_for_status(status)
+ if detailed_status?(status)
+ return custom_icon(status.icon)
+ end
+
icon_name =
case status
when 'success'
@@ -94,4 +93,10 @@ module CiStatusHelper
class: klass, title: title, data: data
end
end
+
+ def detailed_status?(status)
+ status.respond_to?(:text) &&
+ status.respond_to?(:label) &&
+ status.respond_to?(:icon)
+ end
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index ed402b698fb..66a720a9426 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -130,7 +130,7 @@ module CommitsHelper
def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
return unless current_user
- tooltip = "Revert this #{commit.change_type_title} in a new merge request" if has_tooltip
+ tooltip = "Revert this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip
if can_collaborate_with_project?
btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil?
@@ -154,7 +154,7 @@ module CommitsHelper
def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
return unless current_user
- tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request"
+ tooltip = "Cherry-pick this #{commit.change_type_title(current_user)} in a new merge request"
if can_collaborate_with_project?
btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil?
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 0725c3f4c56..c35d6611ab0 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -51,11 +51,12 @@ module DiffHelper
html.html_safe
end
- def diff_line_content(line, line_type = nil)
+ def diff_line_content(line)
if line.blank?
- " &nbsp;".html_safe
+ "&nbsp;".html_safe
else
- line[0] = ' ' if %w[new old].include?(line_type)
+ # We can't use `sub` because the HTML-safeness of `line` will not survive.
+ line[0] = '' if line.start_with?('+', '-', ' ')
line
end
end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index cbab1fd5967..81e0b6bb5ae 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -43,7 +43,7 @@ module DropdownsHelper
default_label = data_attr[:default_label]
content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}")
- output << icon('caret-down')
+ output << icon('chevron-down')
output.html_safe
end
end
diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb
new file mode 100644
index 00000000000..ff8550439d0
--- /dev/null
+++ b/app/helpers/environment_helper.rb
@@ -0,0 +1,31 @@
+module EnvironmentHelper
+ def environment_for_build(project, build)
+ return unless build.environment
+
+ project.environments.find_by(name: build.expanded_environment_name)
+ end
+
+ def environment_link_for_build(project, build)
+ environment = environment_for_build(project, build)
+ if environment
+ link_to environment.name, namespace_project_environment_path(project.namespace, project, environment)
+ else
+ content_tag :span, build.expanded_environment_name
+ end
+ end
+
+ def deployment_link(deployment, text: nil)
+ return unless deployment
+
+ link_label = text ? text : "##{deployment.iid}"
+
+ link_to link_label, [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
+ end
+
+ def last_deployment_link_for_environment_build(project, build)
+ environment = environment_for_build(project, build)
+ return unless environment
+
+ deployment_link(environment.last_deployment)
+ end
+end
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
new file mode 100644
index 00000000000..515e802e01e
--- /dev/null
+++ b/app/helpers/environments_helper.rb
@@ -0,0 +1,7 @@
+module EnvironmentsHelper
+ def environments_list_data
+ {
+ endpoint: namespace_project_environments_path(@project.namespace, @project, format: :json)
+ }
+ end
+end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 00e64076408..362046c0270 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -45,6 +45,12 @@ module EventsHelper
@project.feature_available?(feature_key, current_user)
end
+ def comments_visible?
+ event_filter_visible(:repository) ||
+ event_filter_visible(:merge_requests) ||
+ event_filter_visible(:issues)
+ end
+
def event_preposition(event)
if event.push? || event.commented? || event.target
"at"
@@ -86,7 +92,7 @@ module EventsHelper
elsif event.merge_request?
namespace_project_merge_request_url(event.project.namespace,
event.project, event.merge_request)
- elsif event.note? && event.commit_note?
+ elsif event.commit_note?
namespace_project_commit_url(event.project.namespace, event.project,
event.note_target)
elsif event.note?
@@ -127,7 +133,7 @@ module EventsHelper
end
def event_note_target_path(event)
- if event.note? && event.commit_note?
+ if event.commit_note?
namespace_project_commit_path(event.project.namespace,
event.project,
event.note_target,
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index 6a43be2cf3e..1182939f656 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -7,12 +7,12 @@ module FormHelper
content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do
content_tag(:h4, headline) <<
- content_tag(:ul) do
- model.errors.full_messages.
- map { |msg| content_tag(:li, msg) }.
- join.
- html_safe
- end
+ content_tag(:ul) do
+ model.errors.full_messages.
+ map { |msg| content_tag(:li, msg) }.
+ join.
+ html_safe
+ end
end
end
end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index 0772d848289..eb435cc1783 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -174,7 +174,7 @@ module GitlabMarkdownHelper
# Returns a String
def cross_project_reference(project, entity)
if entity.respond_to?(:to_reference)
- "#{project.to_reference}#{entity.to_reference}"
+ entity.to_reference(project)
else
''
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index bccf64d1aac..99db73c9ee0 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -82,6 +82,10 @@ module GitlabRoutingHelper
namespace_project_merge_request_path(entity.project.namespace, entity.project, entity, *args)
end
+ def pipeline_path(pipeline, *args)
+ namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, *args)
+ end
+
def milestone_path(entity, *args)
namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args)
end
@@ -155,6 +159,11 @@ module GitlabRoutingHelper
resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end
+ # Snippets
+ def personal_snippet_url(snippet, *args)
+ snippet_url(snippet)
+ end
+
# Groups
## Members
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index ab880ed6de0..77dc9e7d538 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -5,22 +5,25 @@ module GroupsHelper
def group_icon(group)
if group.is_a?(String)
- group = Group.find_by(path: group)
+ group = Group.find_by_full_path(group)
end
- if group && group.avatar.present?
- group.avatar.url
- else
- image_path('no_group_avatar.png')
- end
+ group.try(:avatar_url) || image_path('no_group_avatar.png')
end
def group_title(group, name = nil, url = nil)
- full_title = link_to(simple_sanitize(group.name), group_path(group))
+ full_title = ''
+
+ group.parents.each do |parent|
+ full_title += link_to(simple_sanitize(parent.name), group_path(parent))
+ full_title += ' / '.html_safe
+ end
+
+ full_title += link_to(simple_sanitize(group.name), group_path(group))
full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url) if name
content_tag :span do
- full_title
+ full_title.html_safe
end
end
@@ -48,4 +51,8 @@ module GroupsHelper
"#{status.humanize} #{projects_lfs_status(group)}"
end
end
+
+ def group_issues(group)
+ IssuesFinder.new(current_user, group_id: group.id).execute
+ end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 8127c3f3ee3..8231f8fa334 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -30,11 +30,6 @@ module IssuablesHelper
end
end
- def can_add_template?(issuable)
- names = issuable_templates(issuable)
- names.empty? && can?(current_user, :push_code, @project) && !@project.private?
- end
-
def template_dropdown_tag(issuable, &block)
title = selected_template(issuable) || "Choose a template"
options = {
@@ -141,8 +136,33 @@ module IssuablesHelper
html.html_safe
end
+ def cached_assigned_issuables_count(assignee, issuable_type, state)
+ cache_key = hexdigest(['assigned_issuables_count', assignee.id, issuable_type, state].join('-'))
+ Rails.cache.fetch(cache_key, expires_in: 2.minutes) do
+ assigned_issuables_count(assignee, issuable_type, state)
+ end
+ end
+
+ def issuable_filter_params
+ [
+ :search,
+ :author_id,
+ :assignee_id,
+ :milestone_title,
+ :label_name
+ ]
+ end
+
+ def issuable_filter_present?
+ issuable_filter_params.any? { |k| params.key?(k) }
+ end
+
private
+ def assigned_issuables_count(assignee, issuable_type, state)
+ assignee.public_send("assigned_#{issuable_type}").public_send(state).count
+ end
+
def sidebar_gutter_collapsed?
cookies[:collapsed_gutter] == 'true'
end
@@ -159,15 +179,10 @@ module IssuablesHelper
end
end
- def issuable_filters_present
- params[:search] || params[:author_id] || params[:assignee_id] || params[:milestone_title] || params[:label_name]
- end
-
def issuables_count_for_state(issuable_type, state)
- issuables_finder = public_send("#{issuable_type}_finder")
- issuables_finder.params[:state] = state
-
- issuables_finder.execute.page(1).total_count
+ @counts ||= {}
+ @counts[issuable_type] ||= public_send("#{issuable_type}_finder").count_by_state
+ @counts[issuable_type][state]
end
IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page]
@@ -177,6 +192,7 @@ module IssuablesHelper
opts = params.with_indifferent_access
opts[:state] = state
opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY)
+ opts.delete_if { |_, value| value.blank? }
hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-'))
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 1644c346dd8..a8a49e43b05 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -64,6 +64,8 @@ module IssuesHelper
'status-box-merged'
elsif item.closed?
'status-box-closed'
+ elsif item.respond_to?(:upcoming?) && item.upcoming?
+ 'status-box-upcoming'
else
'status-box-open'
end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 221a84b042f..e5b1e6e8bc7 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -68,14 +68,6 @@ module LabelsHelper
end
end
- def toggle_subscription_data(label)
- return unless label.is_a?(ProjectLabel)
-
- {
- url: toggle_subscription_namespace_project_label_path(label.project.namespace, label.project, label)
- }
- end
-
def render_colored_label(label, label_suffix = '', tooltip: true)
label_color = label.color || Label::DEFAULT_COLOR
text_color = text_color_for_bg(label_color)
@@ -90,12 +82,6 @@ module LabelsHelper
span.html_safe
end
- def render_colored_cross_project_label(label, source_project = nil, tooltip: true)
- label_suffix = source_project ? source_project.name_with_namespace : label.project.name_with_namespace
- label_suffix = " <i>in #{escape_once(label_suffix)}</i>"
- render_colored_label(label, label_suffix, tooltip: tooltip)
- end
-
def suggested_colors
[
'#0033CC',
@@ -148,20 +134,24 @@ module LabelsHelper
end
end
- def label_subscription_status(label)
- case label
- when GroupLabel then 'Subscribing to group labels is currently not supported.'
- when ProjectLabel then label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
- end
+ def label_subscription_status(label, project)
+ return 'project-level' if label.subscribed?(current_user, project)
+ return 'group-level' if label.subscribed?(current_user)
+
+ 'unsubscribed'
end
- def label_subscription_toggle_button_text(label)
- case label
- when GroupLabel then 'Subscribing to group labels is currently not supported.'
- when ProjectLabel then label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
+ def group_label_unsubscribe_path(label, project)
+ case label_subscription_status(label, project)
+ when 'project-level' then toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)
+ when 'group-level' then toggle_subscription_group_label_path(label.group, label)
end
end
+ def label_subscription_toggle_button_text(label, project)
+ label.subscribed?(current_user, project) ? 'Unsubscribe' : 'Subscribe'
+ end
+
def label_deletion_confirm_text(label)
case label
when GroupLabel then 'Remove this label? This will affect all projects within the group. Are you sure?'
@@ -170,6 +160,5 @@ module LabelsHelper
end
# Required for Banzai::Filter::LabelReferenceFilter
- module_function :render_colored_label, :render_colored_cross_project_label,
- :text_color_for_bg, :escape_once
+ module_function :render_colored_label, :text_color_for_bg, :escape_once
end
diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb
deleted file mode 100644
index 95b60aeab5f..00000000000
--- a/app/helpers/lfs_helper.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-module LfsHelper
- include Gitlab::Routing.url_helpers
-
- def require_lfs_enabled!
- return if Gitlab.config.lfs.enabled
-
- render(
- json: {
- message: 'Git LFS is not enabled on this GitLab server, contact your admin.',
- documentation_url: help_url,
- },
- status: 501
- )
- end
-
- def lfs_check_access!
- return if download_request? && lfs_download_access?
- return if upload_request? && lfs_upload_access?
-
- if project.public? || (user && user.can?(:read_project, project))
- render_lfs_forbidden
- else
- render_lfs_not_found
- end
- end
-
- def lfs_download_access?
- return false unless project.lfs_enabled?
-
- project.public? || ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code?
- end
-
- def user_can_download_code?
- has_authentication_ability?(:download_code) && can?(user, :download_code, project)
- end
-
- def build_can_download_code?
- has_authentication_ability?(:build_download_code) && can?(user, :build_download_code, project)
- end
-
- def lfs_upload_access?
- return false unless project.lfs_enabled?
-
- has_authentication_ability?(:push_code) && can?(user, :push_code, project)
- end
-
- def render_lfs_forbidden
- render(
- json: {
- message: 'Access forbidden. Check your access level.',
- documentation_url: help_url,
- },
- content_type: "application/vnd.git-lfs+json",
- status: 403
- )
- end
-
- def render_lfs_not_found
- render(
- json: {
- message: 'Not found.',
- documentation_url: help_url,
- },
- content_type: "application/vnd.git-lfs+json",
- status: 404
- )
- end
-
- def storage_project
- @storage_project ||= begin
- result = project
-
- loop do
- break unless result.forked?
- result = result.forked_from_project
- end
-
- result
- end
- end
-end
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index 877c77050be..41d471cc92f 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -36,4 +36,12 @@ module MembersHelper
"Are you sure you want to leave the " \
"\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?"
end
+
+ def filter_group_project_member_path(options = {})
+ options = params.slice(:search, :sort).merge(options)
+
+ path = request.path
+ path << "?#{options.to_param}"
+ path
+ end
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index a6659ea2fd1..20218775659 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -59,6 +59,10 @@ module MergeRequestsHelper
@mr_closes_issues ||= @merge_request.closes_issues
end
+ def mr_issues_mentioned_but_not_closing
+ @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing
+ end
+
def mr_change_branches_path(merge_request)
new_namespace_project_merge_request_path(
@project.namespace, @project,
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 83a2a4ad3ec..729928ce1dd 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -86,6 +86,30 @@ module MilestonesHelper
days = milestone.remaining_days
content = content_tag(:strong, days)
content << " #{'day'.pluralize(days)} remaining"
+ elsif milestone.upcoming?
+ content_tag(:strong, 'Upcoming')
+ elsif milestone.start_date && milestone.start_date.past?
+ days = milestone.elapsed_days
+ content = content_tag(:strong, days)
+ content << " #{'day'.pluralize(days)} elapsed"
+ end
+ end
+
+ def milestone_date_range(milestone)
+ if milestone.start_date && milestone.due_date
+ "#{milestone.start_date.to_s(:medium)} - #{milestone.due_date.to_s(:medium)}"
+ elsif milestone.due_date
+ if milestone.due_date.past?
+ "expired on #{milestone.due_date.to_s(:medium)}"
+ else
+ "expires on #{milestone.due_date.to_s(:medium)}"
+ end
+ elsif milestone.start_date
+ if milestone.start_date.past?
+ "started on #{milestone.start_date.to_s(:medium)}"
+ else
+ "starts on #{milestone.start_date.to_s(:medium)}"
+ end
end
end
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index df87fac132d..e21178c7377 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -7,12 +7,12 @@ module NavHelper
def page_gutter_class
if current_path?('merge_requests#show') ||
- current_path?('merge_requests#diffs') ||
- current_path?('merge_requests#commits') ||
- current_path?('merge_requests#builds') ||
- current_path?('merge_requests#conflicts') ||
- current_path?('merge_requests#pipelines') ||
- current_path?('issues#show')
+ current_path?('merge_requests#diffs') ||
+ current_path?('merge_requests#commits') ||
+ current_path?('merge_requests#builds') ||
+ current_path?('merge_requests#conflicts') ||
+ current_path?('merge_requests#pipelines') ||
+ current_path?('issues#show')
if cookies[:collapsed_gutter] == 'true'
"page-gutter right-sidebar-collapsed"
else
@@ -20,6 +20,11 @@ module NavHelper
end
elsif current_path?('builds#show')
"page-gutter build-sidebar right-sidebar-expanded"
+ elsif current_path?('wikis#show') ||
+ current_path?('wikis#edit') ||
+ current_path?('wikis#history') ||
+ current_path?('wikis#git_access')
+ "page-gutter wiki-sidebar right-sidebar-expanded"
end
end
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index 7e8369d0a05..03cc8f2b6bd 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -74,4 +74,13 @@ module NotificationsHelper
return unless notification_setting.source_type
hidden_field_tag "#{notification_setting.source_type.downcase}_id", notification_setting.source_id
end
+
+ def notification_event_name(event)
+ case event
+ when :success_pipeline
+ 'Successful pipeline'
+ else
+ event.to_s.humanize
+ end
+ end
end
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index a46f2c6e17d..6e68aad4cb7 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -50,7 +50,7 @@ module PreferencesHelper
end
def default_project_view
- return 'readme' unless current_user
+ return anonymous_project_view unless current_user
user_view = current_user.project_view
@@ -66,4 +66,8 @@ module PreferencesHelper
"customize_workflow"
end
end
+
+ def anonymous_project_view
+ @project.empty_repo? || !can?(current_user, :download_code, @project) ? 'activity' : 'readme'
+ end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 42c00ec3cd5..d2177f683a1 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -49,10 +49,10 @@ module ProjectsHelper
end
end
- def project_title(project, name = nil, url = nil)
+ def project_title(project)
namespace_link =
if project.group
- link_to(simple_sanitize(project.group.name), group_path(project.group))
+ group_title(project.group)
else
owner = project.namespace.owner
link_to(simple_sanitize(owner.name), user_path(owner))
@@ -66,10 +66,7 @@ module ProjectsHelper
end
end
- full_title = "#{namespace_link} / #{project_link}".html_safe
- full_title << ' &middot; '.html_safe << link_to(simple_sanitize(name), url) if name
-
- full_title
+ "#{namespace_link} / #{project_link}".html_safe
end
def remove_project_message(project)
@@ -394,20 +391,6 @@ module ProjectsHelper
end
end
- def new_readme_path
- ref = @repository.root_ref if @repository
- ref ||= 'master'
-
- namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'README.md')
- end
-
- def new_license_path
- ref = @repository.root_ref if @repository
- ref ||= 'master'
-
- namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'LICENSE')
- end
-
def readme_cache_key
sha = @project.commit.try(:sha) || 'nil'
[@project.path_with_namespace, sha, "readme"].join('-')
@@ -458,4 +441,8 @@ module ProjectsHelper
def project_child_container_class(view_path)
view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}"
end
+
+ def project_issues(project)
+ IssuesFinder.new(current_user, project_id: project.id).execute
+ end
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index aba3a3f9c5d..cdb9663877c 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -31,34 +31,7 @@ module SearchHelper
end
def parse_search_result(result)
- ref = nil
- filename = nil
- basename = nil
- startline = 0
-
- result.each_line.each_with_index do |line, index|
- if line =~ /^.*:.*:\d+:/
- ref, filename, startline = line.split(':')
- startline = startline.to_i - index
- extname = Regexp.escape(File.extname(filename))
- basename = filename.sub(/#{extname}$/, '')
- break
- end
- end
-
- data = ""
-
- result.each_line do |line|
- data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '')
- end
-
- OpenStruct.new(
- filename: filename,
- basename: basename,
- ref: ref,
- startline: startline,
- data: data
- )
+ Gitlab::ProjectSearchResults.parse_search_result(result)
end
private
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index 3d4abf76419..9bab140e60a 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -17,6 +17,8 @@ module ServicesHelper
"Event will be triggered when a build status changes"
when "wiki_page"
"Event will be triggered when a wiki page is created/updated"
+ when "commit"
+ "Event will be triggered when a commit is created/updated"
end
end
diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb
index 56749d80bd3..b5017080cfb 100644
--- a/app/helpers/sidekiq_helper.rb
+++ b/app/helpers/sidekiq_helper.rb
@@ -5,15 +5,11 @@ module SidekiqHelper
(?<mem>[\d\.,]+)\s+
(?<state>[DRSTWXZNLsl\+<]+)\s+
(?<start>.+)\s+
- (?<command>sidekiq.*\])\s*
+ (?<command>sidekiq.*\])
\z/x
def parse_sidekiq_ps(line)
- match = line.match(SIDEKIQ_PS_REGEXP)
- if match
- match[1..6]
- else
- %w[? ? ? ? ? ?]
- end
+ match = line.strip.match(SIDEKIQ_PS_REGEXP)
+ match ? match[1..6] : Array.new(6, '?')
end
end
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 7e33a562077..8c02b4061ca 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -8,6 +8,17 @@ module SnippetsHelper
end
end
+ # Return the path of a snippets index for a user or for a project
+ #
+ # @returns String, path to snippet index
+ def subject_snippets_path(subject = nil, opts = nil)
+ if subject.is_a?(Project)
+ namespace_project_snippets_path(subject.namespace, subject, opts)
+ else # assume subject === User
+ dashboard_snippets_path(opts)
+ end
+ end
+
# Get an array of line numbers surrounding a matching
# line, bounded by min/max.
#
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 8b138a8e69f..f03c4627050 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -25,7 +25,7 @@ module SortingHelper
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated,
sort_value_recently_created => sort_title_recently_created,
- sort_value_oldest_created => sort_title_oldest_created,
+ sort_value_oldest_created => sort_title_oldest_created
}
if current_controller?('admin/projects')
@@ -35,6 +35,19 @@ module SortingHelper
options
end
+ def member_sort_options_hash
+ {
+ sort_value_access_level_asc => sort_title_access_level_asc,
+ sort_value_access_level_desc => sort_title_access_level_desc,
+ sort_value_last_joined => sort_title_last_joined,
+ sort_value_oldest_joined => sort_title_oldest_joined,
+ sort_value_name => sort_title_name_asc,
+ sort_value_name_desc => sort_title_name_desc,
+ sort_value_recently_signin => sort_title_recently_signin,
+ sort_value_oldest_signin => sort_title_oldest_signin
+ }
+ end
+
def sort_title_priority
'Priority'
end
@@ -95,6 +108,50 @@ module SortingHelper
'Most popular'
end
+ def sort_title_last_joined
+ 'Last joined'
+ end
+
+ def sort_title_oldest_joined
+ 'Oldest joined'
+ end
+
+ def sort_title_access_level_asc
+ 'Access level, ascending'
+ end
+
+ def sort_title_access_level_desc
+ 'Access level, descending'
+ end
+
+ def sort_title_name_asc
+ 'Name, ascending'
+ end
+
+ def sort_title_name_desc
+ 'Name, descending'
+ end
+
+ def sort_value_last_joined
+ 'last_joined'
+ end
+
+ def sort_value_oldest_joined
+ 'oldest_joined'
+ end
+
+ def sort_value_access_level_asc
+ 'access_level_asc'
+ end
+
+ def sort_value_access_level_desc
+ 'access_level_desc'
+ end
+
+ def sort_value_name_desc
+ 'name_desc'
+ end
+
def sort_value_priority
'priority'
end
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index 563ddd2a511..547f6258909 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -106,9 +106,9 @@ module TabHelper
def branches_tab_class
if current_controller?(:protected_branches) ||
- current_controller?(:branches) ||
- current_page?(namespace_project_repository_path(@project.namespace,
- @project))
+ current_controller?(:branches) ||
+ current_page?(namespace_project_repository_path(@project.namespace,
+ @project))
'active'
end
end
diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb
index 8cad994d10f..b0135ea2e95 100644
--- a/app/helpers/triggers_helper.rb
+++ b/app/helpers/triggers_helper.rb
@@ -1,5 +1,13 @@
module TriggersHelper
- def builds_trigger_url(project_id)
- "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds"
+ def builds_trigger_url(project_id, ref: nil)
+ if ref.nil?
+ "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds"
+ else
+ "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/ref/#{ref}/trigger/builds"
+ end
+ end
+
+ def service_trigger_url(service)
+ "#{Settings.gitlab.url}/api/v3/projects/#{service.project_id}/services/#{service.to_param}/trigger"
end
end
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index 96116e916dd..0d20c9092c4 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -4,6 +4,7 @@ module Emails
setup_note_mail(note_id, recipient_id)
@commit = @note.noteable
+ @discussion = @note.to_discussion if @note.diff_note?
@target_url = namespace_project_commit_url(*note_target_url_options)
mail_answer_thread(@commit,
@@ -24,6 +25,7 @@ module Emails
setup_note_mail(note_id, recipient_id)
@merge_request = @note.noteable
+ @discussion = @note.to_discussion if @note.diff_note?
@target_url = namespace_project_merge_request_url(*note_target_url_options)
mail_answer_thread(@merge_request, note_thread_options(recipient_id))
end
diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb
index 601c8b5cd62..9460a6cd2be 100644
--- a/app/mailers/emails/pipelines.rb
+++ b/app/mailers/emails/pipelines.rb
@@ -1,22 +1,27 @@
module Emails
module Pipelines
- def pipeline_success_email(pipeline, to)
- pipeline_mail(pipeline, to, 'succeeded')
+ def pipeline_success_email(pipeline, recipients)
+ pipeline_mail(pipeline, recipients, 'succeeded')
end
- def pipeline_failed_email(pipeline, to)
- pipeline_mail(pipeline, to, 'failed')
+ def pipeline_failed_email(pipeline, recipients)
+ pipeline_mail(pipeline, recipients, 'failed')
end
private
- def pipeline_mail(pipeline, to, status)
+ def pipeline_mail(pipeline, recipients, status)
@project = pipeline.project
@pipeline = pipeline
@merge_request = pipeline.merge_requests.first
add_headers
- mail(to: to, subject: pipeline_subject(status), skip_premailer: true) do |format|
+ # We use bcc here because we don't want to generate this emails for a
+ # thousand times. This could be potentially expensive in a loop, and
+ # recipients would contain all project watchers so it could be a lot.
+ mail(bcc: recipients,
+ subject: pipeline_subject(status),
+ skip_premailer: true) do |format|
format.html { render layout: false }
format.text
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index bb60cc8736c..bf463a3b6bb 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -19,6 +19,7 @@ class ApplicationSetting < ActiveRecord::Base
serialize :domain_whitelist, Array
serialize :domain_blacklist, Array
serialize :repository_storages
+ serialize :sidekiq_throttling_queues, Array
cache_markdown_field :sign_in_text
cache_markdown_field :help_page_text
@@ -85,6 +86,15 @@ class ApplicationSetting < ActiveRecord::Base
presence: { message: 'Domain blacklist cannot be empty if Blacklist is enabled.' },
if: :domain_blacklist_enabled?
+ validates :sidekiq_throttling_factor,
+ numericality: { greater_than: 0, less_than: 1 },
+ presence: { message: 'Throttling factor cannot be empty if Sidekiq Throttling is enabled.' },
+ if: :sidekiq_throttling_enabled?
+
+ validates :sidekiq_throttling_queues,
+ presence: { message: 'Queues to throttle cannot be empty if Sidekiq Throttling is enabled.' },
+ if: :sidekiq_throttling_enabled?
+
validates :housekeeping_incremental_repack_period,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -180,6 +190,7 @@ class ApplicationSetting < ActiveRecord::Base
container_registry_token_expire_delay: 5,
repository_storages: ['default'],
user_default_external: false,
+ sidekiq_throttling_enabled: false,
housekeeping_enabled: true,
housekeeping_bitmaps_enabled: true,
housekeeping_incremental_repack_period: 10,
@@ -192,6 +203,10 @@ class ApplicationSetting < ActiveRecord::Base
ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url)
end
+ def sidekiq_throttling_column_exists?
+ ActiveRecord::Base.connection.column_exists?(:application_settings, :sidekiq_throttling_enabled)
+ end
+
def domain_whitelist_raw
self.domain_whitelist.join("\n") unless self.domain_whitelist.nil?
end
@@ -245,6 +260,12 @@ class ApplicationSetting < ActiveRecord::Base
ensure_health_check_access_token!
end
+ def sidekiq_throttling_enabled?
+ return false unless sidekiq_throttling_column_exists?
+
+ sidekiq_throttling_enabled
+ end
+
private
def check_repository_storages
diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb
new file mode 100644
index 00000000000..f321db75eeb
--- /dev/null
+++ b/app/models/chat_name.rb
@@ -0,0 +1,12 @@
+class ChatName < ActiveRecord::Base
+ belongs_to :service
+ belongs_to :user
+
+ validates :user, presence: true
+ validates :service, presence: true
+ validates :team_id, presence: true
+ validates :chat_id, presence: true
+
+ validates :user_id, uniqueness: { scope: [:service_id] }
+ validates :chat_id, uniqueness: { scope: [:service_id, :team_id] }
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index bf5f92f8462..591aba6bdc9 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -7,6 +7,16 @@ module Ci
belongs_to :trigger_request
belongs_to :erased_by, class_name: 'User'
+ has_many :deployments, as: :deployable
+
+ # The "environment" field for builds is a String, and is the unexpanded name
+ def persisted_environment
+ @persisted_environment ||= Environment.find_by(
+ name: expanded_environment_name,
+ project_id: gl_project_id
+ )
+ end
+
serialize :options
serialize :yaml_variables
@@ -68,7 +78,11 @@ module Ci
environment: build.environment,
status_event: 'enqueue'
)
- MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
+
+ MergeRequests::AddTodoWhenBuildFailsService
+ .new(build.project, nil)
+ .close(new_build)
+
build.pipeline.mark_as_processable_after_stage(build.stage_idx)
new_build
end
@@ -94,6 +108,12 @@ module Ci
end
end
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::Build::Factory
+ .new(self, current_user)
+ .fabricate!
+ end
+
def manual?
self.when == 'manual'
end
@@ -117,14 +137,47 @@ module Ci
end
end
+ def cancelable?
+ active?
+ end
+
def retryable?
- project.builds_enabled? && commands.present? && complete?
+ project.builds_enabled? && commands.present? &&
+ (success? || failed? || canceled?)
end
def retried?
!self.pipeline.statuses.latest.include?(self)
end
+ def expanded_environment_name
+ ExpandVariables.expand(environment, simple_variables) if environment
+ end
+
+ def has_environment?
+ environment.present?
+ end
+
+ def starts_environment?
+ has_environment? && self.environment_action == 'start'
+ end
+
+ def stops_environment?
+ has_environment? && self.environment_action == 'stop'
+ end
+
+ def environment_action
+ self.options.fetch(:environment, {}).fetch(:action, 'start') if self.options
+ end
+
+ def outdated_deployment?
+ success? && !last_deployment.try(:last?)
+ end
+
+ def last_deployment
+ deployments.last
+ end
+
def depends_on_builds
# Get builds of the same type
latest_builds = self.pipeline.builds.latest
@@ -150,12 +203,25 @@ module Ci
project.build_timeout
end
- def variables
+ # A slugified version of the build ref, suitable for inclusion in URLs and
+ # domain names. Rules:
+ #
+ # * Lowercased
+ # * Anything not matching [a-z0-9-] is replaced with a -
+ # * Maximum length is 63 bytes
+ def ref_slug
+ slugified = ref.to_s.downcase
+ slugified.gsub(/[^a-z0-9]/, '-')[0..62]
+ end
+
+ # Variables whose value does not depend on other variables
+ def simple_variables
variables = predefined_variables
variables += project.predefined_variables
variables += pipeline.predefined_variables
variables += runner.predefined_variables if runner
variables += project.container_registry_variables
+ variables += project.deployment_variables if has_environment?
variables += yaml_variables
variables += user_variables
variables += project.secret_variables
@@ -163,13 +229,20 @@ module Ci
variables
end
+ # All variables, including those dependent on other variables
+ def variables
+ variables = simple_variables
+ variables += persisted_environment.predefined_variables if persisted_environment.present?
+ variables
+ end
+
def merge_request
merge_requests = MergeRequest.includes(:merge_request_diff)
.where(source_branch: ref, source_project_id: pipeline.gl_project_id)
.reorder(iid: :asc)
merge_requests.find do |merge_request|
- merge_request.commits.any? { |ci| ci.id == pipeline.sha }
+ merge_request.commits_sha.include?(pipeline.sha)
end
end
@@ -271,6 +344,7 @@ module Ci
def append_trace(trace_part, offset)
recreate_trace_dir
+ touch if needs_touch?
trace_part = hide_secrets(trace_part)
@@ -280,6 +354,10 @@ module Ci
end
end
+ def needs_touch?
+ Time.now - updated_at > 15.minutes.to_i
+ end
+
def trace_file_path
if has_old_trace_file?
old_path_to_trace
@@ -448,6 +526,10 @@ module Ci
]
end
+ def credentials
+ Gitlab::Ci::Build::Credentials::Factory.new(self).create!
+ end
+
private
def update_artifacts_size
@@ -475,6 +557,7 @@ module Ci
{ key: 'CI_BUILD_REF', value: sha, public: true },
{ key: 'CI_BUILD_BEFORE_SHA', value: before_sha, public: true },
{ key: 'CI_BUILD_REF_NAME', value: ref, public: true },
+ { key: 'CI_BUILD_REF_SLUG', value: ref_slug, public: true },
{ key: 'CI_BUILD_NAME', value: name, public: true },
{ key: 'CI_BUILD_STAGE', value: stage, public: true },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index d3432632899..48354cdbefb 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -21,8 +21,6 @@ module Ci
after_create :keep_around_commits, unless: :importing?
- delegate :stages, to: :statuses
-
state_machine :status, initial: :created do
event :enqueue do
transition created: :pending
@@ -81,34 +79,79 @@ module Ci
PipelineHooksWorker.perform_async(id)
end
end
+
+ after_transition any => [:success, :failed] do |pipeline|
+ pipeline.run_after_commit do
+ PipelineNotificationWorker.perform_async(pipeline.id)
+ end
+ end
end
# ref can't be HEAD or SHA, can only be branch/tag name
+ scope :latest, ->(ref = nil) do
+ max_id = unscope(:select)
+ .select("max(#{quoted_table_name}.id)")
+ .group(:ref, :sha)
+
+ if ref
+ where(id: max_id, ref: ref)
+ else
+ where(id: max_id)
+ end
+ end
+
+ def self.latest_status(ref = nil)
+ latest(ref).status
+ end
+
def self.latest_successful_for(ref)
- where(ref: ref).order(id: :desc).success.first
+ success.latest(ref).first
end
def self.truncate_sha(sha)
sha[0...8]
end
- def self.stages
- # We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries
- CommitStatus.where(pipeline: pluck(:id)).stages
- end
-
def self.total_duration
where.not(duration: nil).sum(:duration)
end
- def stages_with_latest_statuses
- statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage)
+ def stages_count
+ statuses.select(:stage).distinct.count
+ end
+
+ def stages_name
+ statuses.order(:stage_idx).distinct.
+ pluck(:stage, :stage_idx).map(&:first)
+ end
+
+ def stages
+ status_sql = statuses.latest.where('stage=sg.stage').status_sql
+
+ stages_query = statuses.group('stage').select(:stage)
+ .order('max(stage_idx)')
+
+ stages_with_statuses = CommitStatus.from(stages_query, :sg).
+ pluck('sg.stage', status_sql)
+
+ stages_with_statuses.map do |stage|
+ Ci::Stage.new(self, name: stage.first, status: stage.last)
+ end
+ end
+
+ def artifacts
+ builds.latest.with_artifacts_not_expired
end
def project_id
project.id
end
+ # For now the only user who participates is the user who triggered
+ def participants(_current_user = nil)
+ Array(user)
+ end
+
def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)")
@@ -150,23 +193,27 @@ module Ci
end
def retryable?
- builds.latest.any? do |build|
- (build.failed? || build.canceled?) && build.retryable?
- end
+ builds.latest.failed_or_canceled.any?(&:retryable?)
end
def cancelable?
- builds.running_or_pending.any?
+ statuses.cancelable.any?
end
def cancel_running
- builds.running_or_pending.each(&:cancel)
+ Gitlab::OptimisticLocking.retry_lock(
+ statuses.cancelable) do |cancelable|
+ cancelable.each(&:cancel)
+ end
end
def retry_failed(user)
- builds.latest.failed.select(&:retryable?).each do |build|
- Ci::Build.retry(build, user)
- end
+ Gitlab::OptimisticLocking.retry_lock(
+ builds.latest.failed_or_canceled) do |failed_or_canceled|
+ failed_or_canceled.select(&:retryable?).each do |build|
+ Ci::Build.retry(build, user)
+ end
+ end
end
def mark_as_processable_after_stage(stage_idx)
@@ -302,7 +349,13 @@ module Ci
def merge_requests
@merge_requests ||= project.merge_requests
.where(source_branch: self.ref)
- .select { |merge_request| merge_request.pipeline.try(:id) == self.id }
+ .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id }
+ end
+
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::Pipeline::Factory
+ .new(self, current_user)
+ .fabricate!
end
private
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
new file mode 100644
index 00000000000..7ef59445d77
--- /dev/null
+++ b/app/models/ci/stage.rb
@@ -0,0 +1,39 @@
+module Ci
+ # Currently this is artificial object, constructed dynamically
+ # We should migrate this object to actual database record in the future
+ class Stage
+ include StaticModel
+
+ attr_reader :pipeline, :name
+
+ delegate :project, to: :pipeline
+
+ def initialize(pipeline, name:, status: nil)
+ @pipeline = pipeline
+ @name = name
+ @status = status
+ end
+
+ def to_param
+ name
+ end
+
+ def status
+ @status ||= statuses.latest.status
+ end
+
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::Stage::Factory
+ .new(self, current_user)
+ .fabricate!
+ end
+
+ def statuses
+ @statuses ||= pipeline.statuses.where(stage: name)
+ end
+
+ def builds
+ @builds ||= pipeline.builds.where(stage: name)
+ end
+ end
+end
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 94d9e2b3208..2c8698d8b5d 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -4,10 +4,10 @@ module Ci
belongs_to :project, foreign_key: :gl_project_id
- validates_uniqueness_of :key, scope: :gl_project_id
validates :key,
presence: true,
- length: { within: 0..255 },
+ uniqueness: { scope: :gl_project_id },
+ length: { maximum: 255 },
format: { with: /\A[a-zA-Z0-9_]+\z/,
message: "can contain only letters, digits and '_'." }
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 9e7fde9503d..69cfc47f5bf 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -48,6 +48,10 @@ class Commit
max_lines: DIFF_HARD_LIMIT_LINES,
}
end
+
+ def from_hash(hash, project)
+ new(Gitlab::Git::Commit.new(hash), project)
+ end
end
attr_accessor :raw
@@ -88,19 +92,11 @@ class Commit
end
def to_reference(from_project = nil)
- if cross_project_reference?(from_project)
- project.to_reference + self.class.reference_prefix + self.id
- else
- self.id
- end
+ commit_reference(from_project, id)
end
def reference_link_text(from_project = nil)
- if cross_project_reference?(from_project)
- project.to_reference + self.class.reference_prefix + self.short_id
- else
- self.short_id
- end
+ commit_reference(from_project, short_id)
end
def diff_line_count
@@ -232,13 +228,9 @@ class Commit
def status(ref = nil)
@statuses ||= {}
- if @statuses.key?(ref)
- @statuses[ref]
- elsif ref
- @statuses[ref] = pipelines.where(ref: ref).status
- else
- @statuses[ref] = pipelines.status
- end
+ return @statuses[ref] if @statuses.key?(ref)
+
+ @statuses[ref] = pipelines.latest_status(ref)
end
def revert_branch_name
@@ -249,44 +241,47 @@ class Commit
project.repository.next_branch("cherry-pick-#{short_id}", mild: true)
end
- def revert_description
- if merged_merge_request
- "This reverts merge request #{merged_merge_request.to_reference}"
+ def revert_description(user)
+ if merged_merge_request?(user)
+ "This reverts merge request #{merged_merge_request(user).to_reference}"
else
"This reverts commit #{sha}"
end
end
- def revert_message
- %Q{Revert "#{title.strip}"\n\n#{revert_description}}
+ def revert_message(user)
+ %Q{Revert "#{title.strip}"\n\n#{revert_description(user)}}
end
- def reverts_commit?(commit)
- description? && description.include?(commit.revert_description)
+ def reverts_commit?(commit, user)
+ description? && description.include?(commit.revert_description(user))
end
def merge_commit?
parents.size > 1
end
- def merged_merge_request
- return @merged_merge_request if defined?(@merged_merge_request)
+ def merged_merge_request(current_user)
+ # Memoize with per-user access check
+ @merged_merge_request_hash ||= Hash.new do |hash, user|
+ hash[user] = merged_merge_request_no_cache(user)
+ end
- @merged_merge_request = project.merge_requests.find_by(merge_commit_sha: id) if merge_commit?
+ @merged_merge_request_hash[current_user]
end
- def has_been_reverted?(current_user = nil, noteable = self)
+ def has_been_reverted?(current_user, noteable = self)
ext = all_references(current_user)
noteable.notes_with_associations.system.each do |note|
note.all_references(current_user, extractor: ext)
end
- ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self) }
+ ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self, current_user) }
end
- def change_type_title
- merged_merge_request ? 'merge request' : 'commit'
+ def change_type_title(user)
+ merged_merge_request?(user) ? 'merge request' : 'commit'
end
# Get the URI type of the given path
@@ -325,6 +320,16 @@ class Commit
private
+ def commit_reference(from_project, referable_commit_id)
+ reference = project.to_reference(from_project)
+
+ if reference.present?
+ "#{reference}#{self.class.reference_prefix}#{referable_commit_id}"
+ else
+ referable_commit_id
+ end
+ end
+
def find_author_by_any_email
User.find_by_any_email(author_email.downcase)
end
@@ -344,4 +349,12 @@ class Commit
changes
end
+
+ def merged_merge_request?(user)
+ !!merged_merge_request(user)
+ end
+
+ def merged_merge_request_no_cache(user)
+ MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit?
+ end
end
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index ac2477fd973..d9af7f6c139 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -90,21 +90,24 @@ class CommitRange
alias_method :id, :to_s
def to_reference(from_project = nil)
- if cross_project_reference?(from_project)
- project.to_reference + self.class.reference_prefix + self.id
+ project_reference = project.to_reference(from_project)
+
+ if project_reference.present?
+ project_reference + self.class.reference_prefix + self.id
else
self.id
end
end
def reference_link_text(from_project = nil)
- reference = ref_from + notation + ref_to
+ project_reference = project.to_reference(from_project)
+ reference = ref_from + notation + ref_to
- if cross_project_reference?(from_project)
- reference = project.to_reference + self.class.reference_prefix + reference
+ if project_reference.present?
+ project_reference + self.class.reference_prefix + reference
+ else
+ reference
end
-
- reference
end
# Return a Hash of parameters for passing to a URL helper
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index d159fc6c5c7..31cd381dcd2 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -31,18 +31,13 @@ class CommitStatus < ActiveRecord::Base
end
scope :exclude_ignored, -> do
- quoted_when = connection.quote_column_name('when')
# We want to ignore failed_but_allowed jobs
where("allow_failure = ? OR status IN (?)",
- false, all_state_names - [:failed, :canceled]).
- # We want to ignore skipped manual jobs
- where("#{quoted_when} <> ? OR status <> ?", 'manual', 'skipped').
- # We want to ignore skipped on_failure
- where("#{quoted_when} <> ? OR status <> ?", 'on_failure', 'skipped')
+ false, all_state_names - [:failed, :canceled])
end
- scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) }
- scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) }
+ scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) }
+ scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
state_machine :status do
event :enqueue do
@@ -117,33 +112,29 @@ class CommitStatus < ActiveRecord::Base
name.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip
end
- def self.stages
- # We group by stage name, but order stages by theirs' index
- unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage')
- end
-
- def self.stages_status
- # We execute subquery for each stage to calculate a stage status
- statuses = unscoped.from(all, :sg).group('stage').pluck('sg.stage', all.where('stage=sg.stage').status_sql)
- statuses.inject({}) do |h, k|
- h[k.first] = k.last
- h
- end
- end
-
def failed_but_allowed?
allow_failure? && (failed? || canceled?)
end
+ def duration
+ calculate_duration
+ end
+
def playable?
false
end
- def duration
- calculate_duration
+ def stuck?
+ false
end
- def stuck?
+ def has_trace?
false
end
+
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::Factory
+ .new(self, current_user)
+ .fabricate!
+ end
end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index ef3e73a4072..90432fc4050 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -4,7 +4,7 @@ module HasStatus
AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped]
STARTED_STATUSES = %w[running success failed skipped]
ACTIVE_STATUSES = %w[pending running]
- COMPLETED_STATUSES = %w[success failed canceled]
+ COMPLETED_STATUSES = %w[success failed canceled skipped]
ORDERED_STATUSES = %w[failed pending running canceled success skipped]
class_methods do
@@ -23,9 +23,10 @@ module HasStatus
canceled = scope.canceled.select('count(*)').to_sql
"(CASE
+ WHEN (#{builds})=(#{skipped}) THEN 'skipped'
WHEN (#{builds})=(#{success}) THEN 'success'
WHEN (#{builds})=(#{created}) THEN 'created'
- WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'skipped'
+ WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'success'
WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled'
WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running'
@@ -73,6 +74,11 @@ module HasStatus
scope :skipped, -> { where(status: 'skipped') }
scope :running_or_pending, -> { where(status: [:running, :pending]) }
scope :finished, -> { where(status: [:success, :failed, :canceled]) }
+ scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) }
+
+ scope :cancelable, -> do
+ where(status: [:running, :pending, :created])
+ end
end
def started?
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 93a6b3122e0..0ea7b1b1098 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -41,7 +41,7 @@ module Issuable
has_one :metrics
validates :author, presence: true
- validates :title, presence: true, length: { within: 0..255 }
+ validates :title, presence: true, length: { maximum: 255 }
scope :authored, ->(user) { where(author_id: user) }
scope :assigned_to, ->(u) { where(assignee_id: u.id)}
@@ -183,6 +183,10 @@ module Issuable
grouping_columns
end
+
+ def to_ability_name
+ model_name.singular
+ end
end
def today?
@@ -211,7 +215,7 @@ module Issuable
end
end
- def subscribed_without_subscriptions?(user)
+ def subscribed_without_subscriptions?(user, project)
participants(user).include?(user)
end
@@ -244,7 +248,18 @@ module Issuable
# issuable.class # => MergeRequest
# issuable.to_ability_name # => "merge_request"
def to_ability_name
- self.class.to_s.underscore
+ self.class.to_ability_name
+ end
+
+ # Convert this Issuable class name to a format usable by notifications.
+ #
+ # Examples:
+ #
+ # issuable.class # => MergeRequest
+ # issuable.human_class_name # => "merge request"
+
+ def human_class_name
+ @human_class_name ||= self.class.name.titleize.downcase
end
# Returns a Hash of attributes to be used for Twitter card metadata
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index eb2ff0428f6..8ab0401d288 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -1,6 +1,6 @@
# == Mentionable concern
#
-# Contains functionality related to objects that can mention Users, Issues, MergeRequests, or Commits by
+# Contains functionality related to objects that can mention Users, Issues, MergeRequests, Commits or Snippets by
# GFM references.
#
# Used by Issue, Note, MergeRequest, and Commit.
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 7bcc78247ba..4359f1d7b06 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -1,17 +1,17 @@
module Milestoneish
- def closed_items_count(user = nil)
+ def closed_items_count(user)
issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size
end
- def total_items_count(user = nil)
+ def total_items_count(user)
issues_visible_to_user(user).size + merge_requests.size
end
- def complete?(user = nil)
+ def complete?(user)
total_items_count(user) > 0 && total_items_count(user) == closed_items_count(user)
end
- def percent_complete(user = nil)
+ def percent_complete(user)
((closed_items_count(user) * 100) / total_items_count(user)).abs
rescue ZeroDivisionError
0
@@ -23,7 +23,31 @@ module Milestoneish
(due_date - Date.today).to_i
end
- def issues_visible_to_user(user = nil)
- issues.visible_to_user(user)
+ def elapsed_days
+ return 0 if !start_date || start_date.future?
+
+ (Date.today - start_date).to_i
+ end
+
+ def issues_visible_to_user(user)
+ IssuesFinder.new(user).execute.where(id: issues)
+ end
+
+ def upcoming?
+ start_date && start_date.future?
+ end
+
+ def expires_at
+ if due_date
+ if due_date.past?
+ "expired on #{due_date.to_s(:medium)}"
+ else
+ "expires on #{due_date.to_s(:medium)}"
+ end
+ end
+ end
+
+ def expired?
+ due_date && due_date.past?
end
end
diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb
index 7fd0905ee81..9dd4d9c6f24 100644
--- a/app/models/concerns/protected_branch_access.rb
+++ b/app/models/concerns/protected_branch_access.rb
@@ -2,6 +2,9 @@ module ProtectedBranchAccess
extend ActiveSupport::Concern
included do
+ belongs_to :protected_branch
+ delegate :project, to: :protected_branch
+
scope :master, -> { where(access_level: Gitlab::Access::MASTER) }
scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) }
end
@@ -9,4 +12,10 @@ module ProtectedBranchAccess
def humanize
self.class.human_access_levels[self.access_level]
end
+
+ def check_access(user)
+ return true if user.is_admin?
+
+ project.team.max_member_access(user.id) >= access_level
+ end
end
diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb
index dee940a3f88..8ba009fe04f 100644
--- a/app/models/concerns/referable.rb
+++ b/app/models/concerns/referable.rb
@@ -72,17 +72,4 @@ module Referable
}x
end
end
-
- private
-
- # Check if a reference is being done cross-project
- #
- # from_project - Refering Project object
- def cross_project_reference?(from_project)
- if self.is_a?(Project)
- self != from_project
- else
- from_project && self.project && self.project != from_project
- end
- end
end
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
new file mode 100644
index 00000000000..1108a64c59e
--- /dev/null
+++ b/app/models/concerns/routable.rb
@@ -0,0 +1,71 @@
+# 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.
+module Routable
+ extend ActiveSupport::Concern
+
+ included do
+ has_one :route, as: :source, autosave: true, dependent: :destroy
+
+ validates_associated :route
+ validates :route, presence: true
+
+ before_validation :update_route_path, if: :full_path_changed?
+ end
+
+ class_methods do
+ # Finds a single object by full path match in routes table.
+ #
+ # Usage:
+ #
+ # Klass.find_by_full_path('gitlab-org/gitlab-ce')
+ #
+ # Returns a single object, or nil.
+ def find_by_full_path(path)
+ # On MySQL we want to ensure the ORDER BY uses a case-sensitive match so
+ # any literal matches come first, for this we have to use "BINARY".
+ # Without this there's still no guarantee in what order MySQL will return
+ # rows.
+ binary = Gitlab::Database.mysql? ? 'BINARY' : ''
+
+ order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)"
+
+ where_full_path_in([path]).reorder(order_sql).take
+ end
+
+ # Builds a relation to find multiple objects by their full paths.
+ #
+ # Usage:
+ #
+ # Klass.where_full_path_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee})
+ #
+ # Returns an ActiveRecord::Relation.
+ def where_full_path_in(paths)
+ wheres = []
+ cast_lower = Gitlab::Database.postgresql?
+
+ paths.each do |path|
+ path = connection.quote(path)
+ where = "(routes.path = #{path})"
+
+ if cast_lower
+ where = "(#{where} OR (LOWER(routes.path) = LOWER(#{path})))"
+ end
+
+ wheres << where
+ end
+
+ if wheres.empty?
+ none
+ else
+ joins(:route).where(wheres.join(' OR '))
+ end
+ end
+ end
+
+ private
+
+ def update_route_path
+ route || build_route(source: self)
+ route.path = full_path
+ end
+end
diff --git a/app/models/concerns/select_for_project_authorization.rb b/app/models/concerns/select_for_project_authorization.rb
new file mode 100644
index 00000000000..50a1d7fc3e1
--- /dev/null
+++ b/app/models/concerns/select_for_project_authorization.rb
@@ -0,0 +1,9 @@
+module SelectForProjectAuthorization
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def select_for_project_authorization
+ select("members.user_id, projects.id AS project_id, members.access_level")
+ end
+ end
+end
diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb
index 083257f1005..83daa9b1a64 100644
--- a/app/models/concerns/subscribable.rb
+++ b/app/models/concerns/subscribable.rb
@@ -12,39 +12,71 @@ module Subscribable
has_many :subscriptions, dependent: :destroy, as: :subscribable
end
- def subscribed?(user)
- if subscription = subscriptions.find_by_user_id(user.id)
+ def subscribed?(user, project = nil)
+ if subscription = subscriptions.find_by(user: user, project: project)
subscription.subscribed
else
- subscribed_without_subscriptions?(user)
+ subscribed_without_subscriptions?(user, project)
end
end
# Override this method to define custom logic to consider a subscribable as
# subscribed without an explicit subscription record.
- def subscribed_without_subscriptions?(user)
+ def subscribed_without_subscriptions?(user, project)
false
end
- def subscribers
- subscriptions.where(subscribed: true).map(&:user)
+ def subscribers(project)
+ subscriptions_available(project).
+ where(subscribed: true).
+ map(&:user)
end
- def toggle_subscription(user)
- subscriptions.
- find_or_initialize_by(user_id: user.id).
- update(subscribed: !subscribed?(user))
+ def toggle_subscription(user, project = nil)
+ unsubscribe_from_other_levels(user, project)
+
+ find_or_initialize_subscription(user, project).
+ update(subscribed: !subscribed?(user, project))
+ end
+
+ def subscribe(user, project = nil)
+ unsubscribe_from_other_levels(user, project)
+
+ find_or_initialize_subscription(user, project)
+ .update(subscribed: true)
+ end
+
+ def unsubscribe(user, project = nil)
+ unsubscribe_from_other_levels(user, project)
+
+ find_or_initialize_subscription(user, project)
+ .update(subscribed: false)
end
- def subscribe(user)
+ private
+
+ def unsubscribe_from_other_levels(user, project)
+ other_subscriptions = subscriptions.where(user: user)
+
+ other_subscriptions =
+ if project.blank?
+ other_subscriptions.where.not(project: nil)
+ else
+ other_subscriptions.where(project: nil)
+ end
+
+ other_subscriptions.update_all(subscribed: false)
+ end
+
+ def find_or_initialize_subscription(user, project)
subscriptions.
- find_or_initialize_by(user_id: user.id).
- update(subscribed: true)
+ find_or_initialize_by(user_id: user.id, project_id: project.try(:id))
end
- def unsubscribe(user)
+ def subscriptions_available(project)
+ t = Subscription.arel_table
+
subscriptions.
- find_or_initialize_by(user_id: user.id).
- update(subscribed: false)
+ where(t[:project_id].eq(nil).or(t[:project_id].eq(project.try(:id))))
end
end
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index 04d30f46210..1ca7f91dc03 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -39,6 +39,10 @@ module TokenAuthenticatable
current_token.blank? ? write_new_token(token_field) : current_token
end
+ define_method("set_#{token_field}") do |token|
+ write_attribute(token_field, token) if token
+ end
+
define_method("ensure_#{token_field}!") do
send("reset_#{token_field}!") if read_attribute(token_field).blank?
read_attribute(token_field)
diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb
index 8ed4a56b19b..ba4ee6fcf9d 100644
--- a/app/models/cycle_analytics.rb
+++ b/app/models/cycle_analytics.rb
@@ -1,103 +1,62 @@
class CycleAnalytics
- include Gitlab::Database::Median
- include Gitlab::Database::DateTime
+ STAGES = %i[issue plan code test review staging production].freeze
- DEPLOYMENT_METRIC_STAGES = %i[production staging]
-
- def initialize(project, from:)
+ def initialize(project, current_user, from:)
@project = project
+ @current_user = current_user
@from = from
+ @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: from, branch: nil)
end
def summary
- @summary ||= Summary.new(@project, from: @from)
+ @summary ||= Summary.new(@project, @current_user, from: @from)
+ end
+
+ def permissions(user:)
+ Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project)
end
def issue
- calculate_metric(:issue,
+ @fetcher.calculate_metric(:issue,
Issue.arel_table[:created_at],
[Issue::Metrics.arel_table[:first_associated_with_milestone_at],
Issue::Metrics.arel_table[:first_added_to_board_at]])
end
def plan
- calculate_metric(:plan,
+ @fetcher.calculate_metric(:plan,
[Issue::Metrics.arel_table[:first_associated_with_milestone_at],
Issue::Metrics.arel_table[:first_added_to_board_at]],
Issue::Metrics.arel_table[:first_mentioned_in_commit_at])
end
def code
- calculate_metric(:code,
+ @fetcher.calculate_metric(:code,
Issue::Metrics.arel_table[:first_mentioned_in_commit_at],
MergeRequest.arel_table[:created_at])
end
def test
- calculate_metric(:test,
+ @fetcher.calculate_metric(:test,
MergeRequest::Metrics.arel_table[:latest_build_started_at],
MergeRequest::Metrics.arel_table[:latest_build_finished_at])
end
def review
- calculate_metric(:review,
+ @fetcher.calculate_metric(:review,
MergeRequest.arel_table[:created_at],
MergeRequest::Metrics.arel_table[:merged_at])
end
def staging
- calculate_metric(:staging,
+ @fetcher.calculate_metric(:staging,
MergeRequest::Metrics.arel_table[:merged_at],
MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
end
def production
- calculate_metric(:production,
+ @fetcher.calculate_metric(:production,
Issue.arel_table[:created_at],
MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
end
-
- private
-
- def calculate_metric(name, start_time_attrs, end_time_attrs)
- cte_table = Arel::Table.new("cte_table_for_#{name}")
-
- # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
- # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
- # We compute the (end_time - start_time) interval, and give it an alias based on the current
- # cycle analytics stage.
- interval_query = Arel::Nodes::As.new(
- cte_table,
- subtract_datetimes(base_query_for(name), end_time_attrs, start_time_attrs, name.to_s))
-
- median_datetime(cte_table, interval_query, name)
- end
-
- # Join table with a row for every <issue,merge_request> pair (where the merge request
- # closes the given issue) with issue and merge request metrics included. The metrics
- # are loaded with an inner join, so issues / merge requests without metrics are
- # automatically excluded.
- def base_query_for(name)
- arel_table = MergeRequestsClosingIssues.arel_table
-
- # Load issues
- query = arel_table.join(Issue.arel_table).on(Issue.arel_table[:id].eq(arel_table[:issue_id])).
- join(Issue::Metrics.arel_table).on(Issue.arel_table[:id].eq(Issue::Metrics.arel_table[:issue_id])).
- where(Issue.arel_table[:project_id].eq(@project.id)).
- where(Issue.arel_table[:deleted_at].eq(nil)).
- where(Issue.arel_table[:created_at].gteq(@from))
-
- # Load merge_requests
- query = query.join(MergeRequest.arel_table, Arel::Nodes::OuterJoin).
- on(MergeRequest.arel_table[:id].eq(arel_table[:merge_request_id])).
- join(MergeRequest::Metrics.arel_table).
- on(MergeRequest.arel_table[:id].eq(MergeRequest::Metrics.arel_table[:merge_request_id]))
-
- if DEPLOYMENT_METRIC_STAGES.include?(name)
- # Limit to merge requests that have been deployed to production after `@from`
- query.where(MergeRequest::Metrics.arel_table[:first_deployed_to_production_at].gteq(@from))
- end
-
- query
- end
end
diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb
index b46db449bf3..82f53d17ddd 100644
--- a/app/models/cycle_analytics/summary.rb
+++ b/app/models/cycle_analytics/summary.rb
@@ -1,12 +1,13 @@
class CycleAnalytics
class Summary
- def initialize(project, from:)
+ def initialize(project, current_user, from:)
@project = project
+ @current_user = current_user
@from = from
end
def new_issues
- @project.issues.created_after(@from).count
+ IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count
end
def commits
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index de06c13481a..bbe813db823 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -25,7 +25,12 @@ class Discussion
to: :last_resolved_note,
allow_nil: true
- delegate :blob, :highlighted_diff_lines, to: :diff_file, allow_nil: true
+ delegate :blob,
+ :highlighted_diff_lines,
+ :diff_lines,
+
+ to: :diff_file,
+ allow_nil: true
def self.for_notes(notes)
notes.group_by(&:discussion_id).values.map { |notes| new(notes) }
@@ -83,6 +88,10 @@ class Discussion
@first_note ||= @notes.first
end
+ def first_note_to_resolve
+ @first_note_to_resolve ||= notes.detect(&:to_be_resolved?)
+ end
+
def last_note
@last_note ||= @notes.last
end
@@ -159,10 +168,11 @@ class Discussion
end
# Returns an array of at most 16 highlighted lines above a diff note
- def truncated_diff_lines
+ def truncated_diff_lines(highlight: true)
+ lines = highlight ? highlighted_diff_lines : diff_lines
prev_lines = []
- highlighted_diff_lines.each do |line|
+ lines.each do |line|
if line.meta?
prev_lines.clear
else
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 73f415c0ef0..8ef1c841ea3 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -1,25 +1,38 @@
class Environment < ActiveRecord::Base
+ # Used to generate random suffixes for the slug
+ NUMBERS = '0'..'9'
+ SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a
+
belongs_to :project, required: true, validate: true
has_many :deployments
before_validation :nullify_external_url
+ before_validation :generate_slug, if: ->(env) { env.slug.blank? }
+
before_save :set_environment_type
validates :name,
presence: true,
uniqueness: { scope: :project_id },
- length: { within: 0..255 },
+ length: { maximum: 255 },
format: { with: Gitlab::Regex.environment_name_regex,
message: Gitlab::Regex.environment_name_regex_message }
+ validates :slug,
+ presence: true,
+ uniqueness: { scope: :project_id },
+ length: { maximum: 24 },
+ format: { with: Gitlab::Regex.environment_slug_regex,
+ message: Gitlab::Regex.environment_slug_regex_message }
+
validates :external_url,
uniqueness: { scope: :project_id },
length: { maximum: 255 },
allow_nil: true,
addressable_url: true
- delegate :stop_action, to: :last_deployment, allow_nil: true
+ delegate :stop_action, :manual_actions, to: :last_deployment, allow_nil: true
scope :available, -> { with_state(:available) }
scope :stopped, -> { with_state(:stopped) }
@@ -37,6 +50,17 @@ class Environment < ActiveRecord::Base
state :stopped
end
+ def predefined_variables
+ [
+ { key: 'CI_ENVIRONMENT_NAME', value: name, public: true },
+ { key: 'CI_ENVIRONMENT_SLUG', value: slug, public: true },
+ ]
+ end
+
+ def recently_updated_on_branch?(ref)
+ ref.to_s == last_deployment.try(:ref)
+ end
+
def last_deployment
deployments.last
end
@@ -92,6 +116,52 @@ class Environment < ActiveRecord::Base
def stop!(current_user)
return unless stoppable?
+ stop
stop_action.play(current_user)
end
+
+ def actions_for(environment)
+ return [] unless manual_actions
+
+ manual_actions.select do |action|
+ action.expanded_environment_name == environment
+ end
+ end
+
+ # An environment name is not necessarily suitable for use in URLs, DNS
+ # or other third-party contexts, so provide a slugified version. A slug has
+ # the following properties:
+ # * contains only lowercase letters (a-z), numbers (0-9), and '-'
+ # * begins with a letter
+ # * has a maximum length of 24 bytes (OpenShift limitation)
+ # * cannot end with `-`
+ def generate_slug
+ # Lowercase letters and numbers only
+ slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-')
+
+ # Must start with a letter
+ slugified = "env-" + slugified if NUMBERS.cover?(slugified[0])
+
+ # 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] == "-"
+
+ # 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
+
+ self.slug = slugified
+ end
+
+ private
+
+ # Slugifying a name may remove the uniqueness guarantee afforded by it being
+ # based on name (which must be unique). To compensate, we add a random
+ # 6-byte suffix in those circumstances. This is not *guaranteed* uniqueness,
+ # but the chance of collisions is vanishingly small
+ def random_suffix
+ (0..5).map { SUFFIX_CHARS.sample }.join
+ end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 43e67069b70..2662f170765 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -43,12 +43,7 @@ class Event < ActiveRecord::Base
scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
class << self
- def reset_event_cache_for(target)
- Event.where(target_id: target.id, target_type: target.class.to_s).
- order('id DESC').limit(100).
- update_all(updated_at: Time.now)
- end
-
+ # Update Gitlab::ContributionsCalendar#activity_dates if this changes
def contributions
where("action = ? OR (target_type in (?) AND action in (?))",
Event::PUSHED, ["MergeRequest", "Issue"],
@@ -61,8 +56,8 @@ class Event < ActiveRecord::Base
end
def visible_to_user?(user = nil)
- if push?
- true
+ if push? || commit_note?
+ Ability.allowed?(user, :download_code, project)
elsif membership_changed?
true
elsif created_project?
@@ -282,7 +277,7 @@ class Event < ActiveRecord::Base
end
def commit_note?
- target.for_commit?
+ note? && target && target.for_commit?
end
def issue_note?
@@ -294,7 +289,7 @@ class Event < ActiveRecord::Base
end
def project_snippet_note?
- target.for_snippet?
+ note? && target && target.for_snippet?
end
def note_target
@@ -352,6 +347,10 @@ class Event < ActiveRecord::Base
update_all(last_activity_at: created_at)
end
+ def authored_by?(user)
+ user ? author_id == user.id : false
+ end
+
private
def recent_update?
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index cde4a568577..b01607dcda9 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -28,26 +28,16 @@ class GlobalMilestone
@title.to_slug.normalize.to_s
end
- def expired?
- if due_date
- due_date.past?
- else
- false
- end
- end
-
def projects
@projects ||= Project.for_milestones(milestones.select(:id))
end
def state
- state = milestones.map { |milestone| milestone.state }
-
- if state.count('closed') == state.size
- 'closed'
- else
- 'active'
+ milestones.each do |milestone|
+ return 'active' if milestone.state != 'closed'
end
+
+ 'closed'
end
def active?
@@ -81,18 +71,15 @@ class GlobalMilestone
@due_date =
if @milestones.all? { |x| x.due_date == @milestones.first.due_date }
@milestones.first.due_date
- else
- nil
end
end
- def expires_at
- if due_date
- if due_date.past?
- "expired on #{due_date.to_s(:medium)}"
- else
- "expires on #{due_date.to_s(:medium)}"
+ def start_date
+ return @start_date if defined?(@start_date)
+
+ @start_date =
+ if @milestones.all? { |x| x.start_date == @milestones.first.start_date }
+ @milestones.first.start_date
end
- end
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index d9e90cd256a..ac8a82c8c1e 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -5,6 +5,7 @@ class Group < Namespace
include Gitlab::VisibilityLevel
include AccessRequestable
include Referable
+ include SelectForProjectAuthorization
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source
alias_method :members, :group_members
@@ -61,6 +62,16 @@ class Group < Namespace
def visible_to_user(user)
where(id: user.authorized_groups.select(:id).reorder(nil))
end
+
+ def select_for_project_authorization
+ if current_scope.joins_values.include?(:shared_projects)
+ joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
+ .where('project_namespace.share_with_group_lock = ?', false)
+ .select("members.user_id, projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level")
+ else
+ super
+ end
+ end
end
def to_reference(_from_project = nil)
@@ -72,7 +83,7 @@ class Group < Namespace
end
def human_name
- name
+ full_name
end
def visibility_level_field
@@ -176,4 +187,8 @@ class Group < Namespace
def system_hook_service
SystemHooksService.new
end
+
+ def refresh_members_authorized_projects
+ UserProjectAccessChangedService.new(users.pluck(:id)).execute
+ end
end
diff --git a/app/models/guest.rb b/app/models/guest.rb
new file mode 100644
index 00000000000..01285ca1264
--- /dev/null
+++ b/app/models/guest.rb
@@ -0,0 +1,7 @@
+class Guest
+ class << self
+ def can?(action, subject)
+ Ability.allowed?(nil, action, subject)
+ end
+ end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 4f02b02c488..738c96e4db3 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -60,61 +60,6 @@ class Issue < ActiveRecord::Base
attributes
end
- class << self
- private
-
- # Returns the project that the current scope belongs to if any, nil otherwise.
- #
- # Examples:
- # - my_project.issues.without_due_date.owner_project => my_project
- # - Issue.all.owner_project => nil
- def owner_project
- # No owner if we're not being called from an association
- return unless all.respond_to?(:proxy_association)
-
- owner = all.proxy_association.owner
-
- # Check if the association is or belongs to a project
- if owner.is_a?(Project)
- owner
- else
- begin
- owner.association(:project).target
- rescue ActiveRecord::AssociationNotFoundError
- nil
- end
- end
- end
- end
-
- def self.visible_to_user(user)
- return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
- return all if user.admin?
-
- # Check if we are scoped to a specific project's issues
- if owner_project
- if owner_project.authorized_for_user?(user, Gitlab::Access::REPORTER)
- # If the project is authorized for the user, they can see all issues in the project
- return all
- else
- # else only non confidential and authored/assigned to them
- return where('issues.confidential IS NULL OR issues.confidential IS FALSE
- OR issues.author_id = :user_id OR issues.assignee_id = :user_id',
- user_id: user.id)
- end
- end
-
- where('
- issues.confidential IS NULL
- OR issues.confidential IS FALSE
- OR (issues.confidential = TRUE
- AND (issues.author_id = :user_id
- OR issues.assignee_id = :user_id
- OR issues.project_id IN(:project_ids)))',
- user_id: user.id,
- project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
- end
-
def self.reference_prefix
'#'
end
@@ -153,11 +98,7 @@ class Issue < ActiveRecord::Base
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}"
- if cross_project_reference?(from_project)
- reference = project.to_reference + reference
- end
-
- reference
+ "#{project.to_reference(from_project)}#{reference}"
end
def referenced_merge_requests(current_user = nil)
@@ -182,18 +123,6 @@ class Issue < ActiveRecord::Base
branches_with_iid - branches_with_merge_request
end
- # Reset issue events cache
- #
- # Since we do cache @event we need to reset cache in special cases:
- # * when an issue is updated
- # Events cache stored like events/23-20130109142513.
- # The cache key includes updated_at timestamp.
- # Thus it will automatically generate a new fragment
- # when the event is updated because the key changes.
- def reset_events_cache
- Event.reset_event_cache_for(self)
- end
-
# To allow polymorphism with MergeRequest.
def source_project
project
@@ -250,29 +179,9 @@ class Issue < ActiveRecord::Base
# Returns `true` if the current issue can be viewed by either a logged in User
# or an anonymous user.
def visible_to_user?(user = nil)
- user ? readable_by?(user) : publicly_visible?
- end
+ return false unless project.feature_available?(:issues, user)
- # Returns `true` if the given User can read the current Issue.
- def readable_by?(user)
- if user.admin?
- true
- elsif project.owner == user
- true
- elsif confidential?
- author == user ||
- assignee == user ||
- project.team.member?(user, Gitlab::Access::REPORTER)
- else
- project.public? ||
- project.internal? && !user.external? ||
- project.team.member?(user)
- end
- end
-
- # Returns `true` if this Issue is visible to everybody.
- def publicly_visible?
- project.public? && !confidential?
+ user ? readable_by?(user) : publicly_visible?
end
def overdue?
@@ -286,7 +195,7 @@ class Issue < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
- json[:subscribed] = subscribed?(options[:user]) if options.has_key?(:user)
+ json[:subscribed] = subscribed?(options[:user], project) if options.has_key?(:user) && options[:user]
if options.has_key?(:labels)
json[:labels] = labels.as_json(
@@ -297,4 +206,32 @@ class Issue < ActiveRecord::Base
end
end
end
+
+ private
+
+ # Returns `true` if the given User can read the current Issue.
+ #
+ # This method duplicates the same check of issue_policy.rb
+ # for performance reasons, check commit: 002ad215818450d2cbbc5fa065850a953dc7ada8
+ # Make sure to sync this method with issue_policy.rb
+ def readable_by?(user)
+ if user.admin?
+ true
+ elsif project.owner == user
+ true
+ elsif confidential?
+ author == user ||
+ assignee == user ||
+ project.team.member?(user, Gitlab::Access::REPORTER)
+ else
+ project.public? ||
+ project.internal? && !user.external? ||
+ project.team.member?(user)
+ end
+ end
+
+ # Returns `true` if this Issue is visible to everybody.
+ def publicly_visible?
+ project.public? && !confidential?
+ end
end
diff --git a/app/models/key.rb b/app/models/key.rb
index 568a60b8af3..a5d25409730 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -6,12 +6,20 @@ class Key < ActiveRecord::Base
belongs_to :user
- before_validation :strip_white_space, :generate_fingerprint
-
- validates :title, presence: true, length: { within: 0..255 }
- validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ }
- validates :key, format: { without: /\n|\r/, message: 'should be a single line' }
- validates :fingerprint, uniqueness: true, presence: { message: 'cannot be generated' }
+ before_validation :generate_fingerprint
+
+ validates :title,
+ presence: true,
+ length: { maximum: 255 }
+ validates :key,
+ presence: true,
+ length: { maximum: 5000 },
+ format: { with: /\A(ssh|ecdsa)-.*\Z/ }
+ validates :key,
+ format: { without: /\n|\r/, message: 'should be a single line' }
+ validates :fingerprint,
+ uniqueness: true,
+ presence: { message: 'cannot be generated' }
delegate :name, :email, to: :user, prefix: true
@@ -21,8 +29,9 @@ class Key < ActiveRecord::Base
after_destroy :remove_from_shell
after_destroy :post_destroy_hook
- def strip_white_space
- self.key = key.strip unless key.blank?
+ def key=(value)
+ value.strip! unless value.blank?
+ write_attribute(:key, value)
end
def publishable_key
diff --git a/app/models/label.rb b/app/models/label.rb
index d9287f2dc29..d38c37344c9 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -144,9 +144,10 @@ class Label < ActiveRecord::Base
#
# Examples:
#
- # Label.first.to_reference # => "~1"
- # Label.first.to_reference(format: :name) # => "~\"bug\""
- # Label.first.to_reference(project1, project2) # => "gitlab-org/gitlab-ce~1"
+ # Label.first.to_reference # => "~1"
+ # Label.first.to_reference(format: :name) # => "~\"bug\""
+ # Label.first.to_reference(project, same_namespace_project) # => "gitlab-ce~1"
+ # Label.first.to_reference(project, another_namespace_project) # => "gitlab-org/gitlab-ce~1"
#
# Returns a String
#
@@ -154,8 +155,8 @@ class Label < ActiveRecord::Base
format_reference = label_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
- if cross_project_reference?(source_project, target_project)
- source_project.to_reference + reference
+ if source_project
+ "#{source_project.to_reference(target_project)}#{reference}"
else
reference
end
@@ -169,10 +170,6 @@ class Label < ActiveRecord::Base
private
- def cross_project_reference?(source_project, target_project)
- source_project && target_project && source_project != target_project
- end
-
def issues_count(user, params = {})
params.merge!(subject_foreign_key => subject.id, label_name: title, scope: 'all')
IssuesFinder.new(user, params.with_indifferent_access).execute.count
diff --git a/app/models/member.rb b/app/models/member.rb
index b89ba8ecbb8..c585e0b450e 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -57,12 +57,18 @@ class Member < ActiveRecord::Base
scope :owners, -> { active.where(access_level: OWNER) }
scope :owners_and_masters, -> { active.where(access_level: [OWNER, MASTER]) }
+ scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) }
+ scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) }
+ scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) }
+ scope :order_oldest_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'ASC')) }
+
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
after_create :send_invite, if: :invite?, unless: :importing?
after_create :send_request, if: :request?, unless: :importing?
after_create :create_notification_setting, unless: [:pending?, :importing?]
after_create :post_create_hook, unless: [:pending?, :importing?]
+ after_create :refresh_member_authorized_projects, if: :importing?
after_update :post_update_hook, unless: [:pending?, :importing?]
after_destroy :post_destroy_hook, unless: :pending?
@@ -71,6 +77,34 @@ class Member < ActiveRecord::Base
default_value_for :notification_level, NotificationSetting.levels[:global]
class << self
+ def search(query)
+ joins(:user).merge(User.search(query))
+ end
+
+ def sort(method)
+ case method.to_s
+ when 'access_level_asc' then reorder(access_level: :asc)
+ when 'access_level_desc' then reorder(access_level: :desc)
+ when 'recent_sign_in' then order_recent_sign_in
+ when 'oldest_sign_in' then order_oldest_sign_in
+ when 'last_joined' then order_created_desc
+ when 'oldest_joined' then order_created_asc
+ else
+ order_by(method)
+ end
+ end
+
+ def left_join_users
+ users = User.arel_table
+ members = Member.arel_table
+
+ member_users = members.join(users, Arel::Nodes::OuterJoin).
+ on(members[:user_id].eq(users[:id])).
+ join_sources
+
+ joins(member_users)
+ end
+
def access_for_user_ids(user_ids)
where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
end
@@ -88,8 +122,8 @@ class Member < ActiveRecord::Base
member =
if user.is_a?(User)
source.members.find_by(user_id: user.id) ||
- source.requesters.find_by(user_id: user.id) ||
- source.members.build(user_id: user.id)
+ source.requesters.find_by(user_id: user.id) ||
+ source.members.build(user_id: user.id)
else
source.members.build(invite_email: user)
end
@@ -113,6 +147,8 @@ class Member < ActiveRecord::Base
member.save
end
+ UserProjectAccessChangedService.new(user.id).execute if user.is_a?(User)
+
member
end
@@ -239,17 +275,28 @@ class Member < ActiveRecord::Base
end
def post_create_hook
+ UserProjectAccessChangedService.new(user.id).execute
system_hook_service.execute_hooks_for(self, :create)
end
def post_update_hook
- # override in subclass
+ UserProjectAccessChangedService.new(user.id).execute if access_level_changed?
end
def post_destroy_hook
+ refresh_member_authorized_projects
system_hook_service.execute_hooks_for(self, :destroy)
end
+ def refresh_member_authorized_projects
+ # If user/source is being destroyed, project access are gonna be destroyed eventually
+ # because of DB foreign keys, so we shouldn't bother with refreshing after each
+ # member is destroyed through association
+ return if destroyed_by_association.present?
+
+ UserProjectAccessChangedService.new(user_id).execute
+ end
+
def after_accept_invite
post_create_hook
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index d76feb9680e..b7c775777c7 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -22,7 +22,8 @@ class MergeRequest < ActiveRecord::Base
after_create :ensure_merge_request_diff, unless: :importing?
after_update :reload_diff_if_branch_changed
- delegate :commits, :real_size, to: :merge_request_diff, prefix: nil
+ delegate :commits, :real_size, :commits_sha, :commits_count,
+ to: :merge_request_diff, prefix: nil
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
@@ -100,7 +101,9 @@ class MergeRequest < ActiveRecord::Base
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
validate :validate_fork, unless: :closed_without_fork?
- scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
+ scope :by_source_or_target_branch, ->(branch_name) do
+ where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
+ end
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
scope :of_projects, ->(ids) { where(target_project_id: ids) }
@@ -175,11 +178,7 @@ class MergeRequest < ActiveRecord::Base
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}"
- if cross_project_reference?(from_project)
- reference = project.to_reference + reference
- end
-
- reference
+ "#{project.to_reference(from_project)}#{reference}"
end
def first_commit
@@ -453,7 +452,7 @@ class MergeRequest < ActiveRecord::Base
should_remove_source_branch? || force_remove_source_branch?
end
- def mr_and_commit_notes
+ def related_notes
# Fetch comments only from last 100 commits
commits_for_notes_limit = 100
commit_ids = commits.last(commits_for_notes_limit).map(&:id)
@@ -469,7 +468,7 @@ class MergeRequest < ActiveRecord::Base
end
def discussions
- @discussions ||= self.mr_and_commit_notes.
+ @discussions ||= self.related_notes.
inc_relations_for_view.
fresh.
discussions
@@ -479,6 +478,14 @@ class MergeRequest < ActiveRecord::Base
@diff_discussions ||= self.notes.diff_notes.discussions
end
+ def resolvable_discussions
+ @resolvable_discussions ||= diff_discussions.select(&:to_be_resolved?)
+ end
+
+ def discussions_can_be_resolved_by?(user)
+ resolvable_discussions.all? { |discussion| discussion.can_resolve?(user) }
+ end
+
def find_diff_discussion(discussion_id)
notes = self.notes.diff_notes.where(discussion_id: discussion_id).fresh.to_a
return if notes.empty?
@@ -494,10 +501,14 @@ class MergeRequest < ActiveRecord::Base
discussions_resolvable? && diff_discussions.none?(&:to_be_resolved?)
end
+ def discussions_to_be_resolved?
+ discussions_resolvable? && !discussions_resolved?
+ end
+
def mergeable_discussions_state?
return true unless project.only_allow_merge_if_all_discussions_are_resolved?
- discussions_resolved?
+ !discussions_to_be_resolved?
end
def hook_attrs
@@ -557,6 +568,19 @@ class MergeRequest < ActiveRecord::Base
end
end
+ def issues_mentioned_but_not_closing(current_user = self.author)
+ return [] unless target_branch == project.default_branch
+
+ ext = Gitlab::ReferenceExtractor.new(project, current_user)
+ ext.analyze(description)
+
+ issues = ext.issues
+ closing_issues = Gitlab::ClosingIssueExtractor.new(project, current_user).
+ closed_by_message(description)
+
+ issues - closing_issues
+ end
+
def target_project_path
if target_project
target_project.path_with_namespace
@@ -601,25 +625,24 @@ class MergeRequest < ActiveRecord::Base
self.target_project.repository.branch_names.include?(self.target_branch)
end
- # Reset merge request events cache
- #
- # Since we do cache @event we need to reset cache in special cases:
- # * when a merge request is updated
- # Events cache stored like events/23-20130109142513.
- # The cache key includes updated_at timestamp.
- # Thus it will automatically generate a new fragment
- # when the event is updated because the key changes.
- def reset_events_cache
- Event.reset_event_cache_for(self)
- end
-
- def merge_commit_message
- message = "Merge branch '#{source_branch}' into '#{target_branch}'\n\n"
- message << "#{title}\n\n"
- message << "#{description}\n\n" if description.present?
+ def merge_commit_message(include_description: false)
+ closes_issues_references = closes_issues.map do |issue|
+ issue.to_reference(target_project)
+ end
+
+ message = [
+ "Merge branch '#{source_branch}' into '#{target_branch}'",
+ title
+ ]
+
+ if !include_description && closes_issues_references.present?
+ message << "Closes #{closes_issues_references.to_sentence}"
+ end
+
+ message << "#{description}" if include_description && description.present?
message << "See merge request #{to_reference}"
- message
+ message.join("\n\n")
end
def reset_merge_when_build_succeeds
@@ -670,7 +693,7 @@ class MergeRequest < ActiveRecord::Base
end
def broken?
- self.commits.blank? || branch_missing? || cannot_be_merged?
+ has_no_commits? || branch_missing? || cannot_be_merged?
end
def can_be_merged_by?(user)
@@ -686,18 +709,21 @@ class MergeRequest < ActiveRecord::Base
def mergeable_ci_state?
return true unless project.only_allow_merge_if_build_succeeds?
- !pipeline || pipeline.success?
+ !head_pipeline || head_pipeline.success? || head_pipeline.skipped?
end
def environments
return [] unless diff_head_commit
- @environments ||=
- begin
- envs = target_project.environments_for(target_branch, diff_head_commit, with_tags: true)
- envs.concat(source_project.environments_for(source_branch, diff_head_commit)) if source_project
- envs.uniq
- end
+ @environments ||= begin
+ target_envs = target_project.environments_for(
+ target_branch, commit: diff_head_commit, with_tags: true)
+
+ source_envs = source_project.environments_for(
+ source_branch, commit: diff_head_commit) if source_project
+
+ (target_envs.to_a + source_envs.to_a).uniq
+ end
end
def state_human_name
@@ -775,18 +801,14 @@ class MergeRequest < ActiveRecord::Base
diverged_commits_count > 0
end
- def commits_sha
- commits.map(&:sha)
- end
-
- def pipeline
+ def head_pipeline
return unless diff_head_sha && source_project
- @pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha)
+ @head_pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha)
end
def all_pipelines
- return unless source_project
+ return Ci::Pipeline.none unless source_project
@all_pipelines ||= source_project.pipelines
.where(sha: all_commits_sha, ref: source_branch)
@@ -809,7 +831,7 @@ class MergeRequest < ActiveRecord::Base
@merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
end
- def can_be_reverted?(current_user = nil)
+ def can_be_reverted?(current_user)
merge_commit && !merge_commit.has_been_reverted?(current_user, self)
end
@@ -876,4 +898,12 @@ class MergeRequest < ActiveRecord::Base
@conflicts_can_be_resolved_in_ui = false
end
end
+
+ def has_commits?
+ commits_count > 0
+ end
+
+ def has_no_commits?
+ !has_commits?
+ end
end
diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb
index 99c49a020c9..cdc408738be 100644
--- a/app/models/merge_request/metrics.rb
+++ b/app/models/merge_request/metrics.rb
@@ -1,5 +1,6 @@
class MergeRequest::Metrics < ActiveRecord::Base
belongs_to :merge_request
+ belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :pipeline_id
def record!
if merge_request.merged? && self.merged_at.blank?
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index dd65a9a8b86..b8f36a2c958 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -11,6 +11,9 @@ class MergeRequestDiff < ActiveRecord::Base
belongs_to :merge_request
+ serialize :st_commits
+ serialize :st_diffs
+
state_machine :state, initial: :empty do
state :collected
state :overflow
@@ -22,8 +25,7 @@ class MergeRequestDiff < ActiveRecord::Base
state :overflow_diff_lines_limit
end
- serialize :st_commits
- serialize :st_diffs
+ scope :viewable, -> { without_state(:empty) }
# All diff information is collected from repository after object is created.
# It allows you to override variables like head_commit_sha before getting diff.
@@ -125,11 +127,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def commits_sha
- if @commits
- commits.map(&:sha)
- else
- st_commits.map { |commit| commit[:id] }
- end
+ st_commits.map { |commit| commit[:id] }
end
def diff_refs
@@ -174,6 +172,10 @@ class MergeRequestDiff < ActiveRecord::Base
CompareService.new.execute(project, head_commit_sha, project, sha, straight: straight)
end
+ def commits_count
+ st_commits.count
+ end
+
private
# Old GitLab implementations may have generated diffs as ["--broken-diff"].
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 23aecbfa3a6..45ca97adad1 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -29,6 +29,7 @@ class Milestone < ActiveRecord::Base
validates :title, presence: true, uniqueness: { scope: :project_id }
validates :project, presence: true
+ validate :start_date_should_be_less_than_due_date, if: Proc.new { |m| m.start_date.present? && m.due_date.present? }
strip_attributes :title
@@ -112,43 +113,22 @@ class Milestone < ActiveRecord::Base
#
# Examples:
#
- # Milestone.first.to_reference # => "%1"
- # Milestone.first.to_reference(format: :name) # => "%\"goal\""
- # Milestone.first.to_reference(project) # => "gitlab-org/gitlab-ce%1"
+ # Milestone.first.to_reference # => "%1"
+ # Milestone.first.to_reference(format: :name) # => "%\"goal\""
+ # Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-ce%1"
+ # Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1"
#
def to_reference(from_project = nil, format: :iid)
format_reference = milestone_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
- if cross_project_reference?(from_project)
- project.to_reference + reference
- else
- reference
- end
+ "#{project.to_reference(from_project)}#{reference}"
end
def reference_link_text(from_project = nil)
self.title
end
- def expired?
- if due_date
- due_date.past?
- else
- false
- end
- end
-
- def expires_at
- if due_date
- if due_date.past?
- "expired on #{due_date.to_s(:medium)}"
- else
- "expires on #{due_date.to_s(:medium)}"
- end
- end
- end
-
def can_be_closed?
active? && issues.opened.count.zero?
end
@@ -212,4 +192,10 @@ class Milestone < ActiveRecord::Base
def sanitize_title(value)
CGI.unescape_html(Sanitize.clean(value.to_s))
end
+
+ def start_date_should_be_less_than_due_date
+ if due_date <= start_date
+ errors.add(:start_date, "Can't be greater than due date")
+ end
+ end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index b67049f0f55..fd42f2328d8 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -4,29 +4,33 @@ class Namespace < ActiveRecord::Base
include CacheMarkdownField
include Sortable
include Gitlab::ShellAdapter
+ include Routable
cache_markdown_field :description, pipeline: :description
has_many :projects, dependent: :destroy
belongs_to :owner, class_name: "User"
+ belongs_to :parent, class_name: "Namespace"
+ has_many :children, class_name: "Namespace", foreign_key: :parent_id
+
validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name,
- length: { within: 0..255 },
- namespace_name: true,
presence: true,
- uniqueness: true
+ uniqueness: { scope: :parent_id },
+ length: { maximum: 255 },
+ namespace_name: true
- validates :description, length: { within: 0..255 }
+ validates :description, length: { maximum: 255 }
validates :path,
- length: { within: 1..255 },
- namespace: true,
presence: true,
- uniqueness: { case_sensitive: false }
+ length: { maximum: 255 },
+ namespace: true
delegate :name, to: :owner, allow_nil: true, prefix: true
after_update :move_dir, if: :path_changed?
+ after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
# Save the storage paths before the projects are destroyed to use them on after destroy
before_destroy(prepend: true) { @old_repository_storage_paths = repository_storage_paths }
@@ -85,7 +89,7 @@ class Namespace < ActiveRecord::Base
end
def to_param
- path
+ full_path
end
def human_name
@@ -103,6 +107,8 @@ class Namespace < ActiveRecord::Base
gitlab_shell.add_namespace(repository_storage_path, path_was)
unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
+ Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"
+
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
raise Exception.new('namespace directory cannot be moved')
@@ -147,6 +153,27 @@ class Namespace < ActiveRecord::Base
Gitlab.config.lfs.enabled
end
+ def full_path
+ if parent
+ parent.full_path + '/' + path
+ else
+ path
+ end
+ end
+
+ def full_name
+ @full_name ||=
+ if parent
+ parent.full_name + ' / ' + name
+ else
+ name
+ end
+ end
+
+ def parents
+ @parents ||= parent ? parent.parents + [parent] : []
+ end
+
private
def repository_storage_paths
@@ -175,4 +202,15 @@ class Namespace < ActiveRecord::Base
end
end
end
+
+ def refresh_access_of_projects_invited_groups
+ Group.
+ joins(project_group_links: :project).
+ where(projects: { namespace_id: id }).
+ find_each(&:refresh_members_authorized_projects)
+ end
+
+ def full_path_changed?
+ path_changed? || parent_id_changed?
+ end
end
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index 345041a6ad1..b524ca50ee8 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -161,8 +161,8 @@ module Network
def is_overlap?(range, overlap_space)
range.each do |i|
if i != range.first &&
- i != range.last &&
- @commits[i].spaces.include?(overlap_space)
+ i != range.last &&
+ @commits[i].spaces.include?(overlap_space)
return true
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 2d644b03e4d..0c1b05dabf2 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -7,6 +7,7 @@ class Note < ActiveRecord::Base
include Importable
include FasterCacheKeys
include CacheMarkdownField
+ include AfterCommitQueue
cache_markdown_field :note, pipeline: :note
@@ -18,6 +19,9 @@ class Note < ActiveRecord::Base
# Banzai::ObjectRenderer
attr_accessor :user_visible_reference_count
+ # Attribute used to store the attributes that have ben changed by slash commands.
+ attr_accessor :commands_changes
+
default_value_for :system, false
attr_mentionable :note, pipeline: :note
@@ -95,7 +99,7 @@ class Note < ActiveRecord::Base
end
def discussions
- Discussion.for_notes(all)
+ Discussion.for_notes(fresh)
end
def grouped_diff_discussions
@@ -103,23 +107,6 @@ class Note < ActiveRecord::Base
Discussion.for_diff_notes(active_notes).
map { |d| [d.line_code, d] }.to_h
end
-
- # Searches for notes matching the given query.
- #
- # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
- #
- # query - The search query as a String.
- # as_user - Limit results to those viewable by a specific user
- #
- # Returns an ActiveRecord::Relation.
- def search(query, as_user: nil)
- table = arel_table
- pattern = "%#{query}%"
-
- Note.joins('LEFT JOIN issues ON issues.id = noteable_id').
- where(table[:note].matches(pattern)).
- merge(Issue.visible_to_user(as_user))
- end
end
def cross_reference?
@@ -197,19 +184,6 @@ class Note < ActiveRecord::Base
super(noteable_type.to_s.classify.constantize.base_class.to_s)
end
- # Reset notes events cache
- #
- # Since we do cache @event we need to reset cache in special cases:
- # * when a note is updated
- # * when a note is removed
- # Events cache stored like events/23-20130109142513.
- # The cache key includes updated_at timestamp.
- # Thus it will automatically generate a new fragment
- # when the event is updated because the key changes.
- def reset_events_cache
- Event.reset_event_cache_for(self)
- end
-
def editable?
!system?
end
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 121b598b8f3..43fc218de2b 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -32,7 +32,9 @@ class NotificationSetting < ActiveRecord::Base
:reopen_merge_request,
:close_merge_request,
:reassign_merge_request,
- :merge_merge_request
+ :merge_merge_request,
+ :failed_pipeline,
+ :success_pipeline
]
store :events, accessors: EMAIL_EVENTS, coder: JSON
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index c4b095e0c04..10a34c42fd8 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -2,6 +2,8 @@ class PersonalAccessToken < ActiveRecord::Base
include TokenAuthenticatable
add_authentication_token_field :token
+ serialize :scopes, Array
+
belongs_to :user
scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") }
diff --git a/app/models/project.rb b/app/models/project.rb
index 4c9c7c001dd..5d5d6737dad 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -13,6 +13,8 @@ class Project < ActiveRecord::Base
include CaseSensitivity
include TokenAuthenticatable
include ProjectFeaturesCompatibility
+ include SelectForProjectAuthorization
+ include Routable
extend Gitlab::ConfigHelper
@@ -23,7 +25,9 @@ class Project < ActiveRecord::Base
cache_markdown_field :description, pipeline: :description
- delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true
+ delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
+ :merge_requests_enabled?, :issues_enabled?, to: :project_feature,
+ allow_nil: true
default_value_for :archived, false
default_value_for :visibility_level, gitlab_config_features.visibility_level
@@ -35,6 +39,7 @@ class Project < ActiveRecord::Base
default_value_for :builds_enabled, gitlab_config_features.builds
default_value_for :wiki_enabled, gitlab_config_features.wiki
default_value_for :snippets_enabled, gitlab_config_features.snippets
+ default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
after_create :ensure_dir_exist
after_create :create_project_feature, unless: :project_feature
@@ -74,9 +79,9 @@ class Project < ActiveRecord::Base
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
has_many :boards, before_add: :validate_board_limit, dependent: :destroy
+ has_many :chat_services
# Project services
- has_many :services
has_one :campfire_service, dependent: :destroy
has_one :drone_ci_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy
@@ -89,7 +94,9 @@ class Project < ActiveRecord::Base
has_one :assembla_service, dependent: :destroy
has_one :asana_service, dependent: :destroy
has_one :gemnasium_service, dependent: :destroy
- has_one :slack_service, dependent: :destroy
+ has_one :mattermost_slash_commands_service, dependent: :destroy
+ has_one :mattermost_notification_service, dependent: :destroy
+ has_one :slack_notification_service, dependent: :destroy
has_one :buildkite_service, dependent: :destroy
has_one :bamboo_service, dependent: :destroy
has_one :teamcity_service, dependent: :destroy
@@ -100,6 +107,7 @@ class Project < ActiveRecord::Base
has_one :bugzilla_service, dependent: :destroy
has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project
has_one :external_wiki_service, dependent: :destroy
+ has_one :kubernetes_service, dependent: :destroy, inverse_of: :project
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
has_one :forked_from_project, through: :forked_project_link
@@ -121,6 +129,8 @@ class Project < ActiveRecord::Base
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy
+ has_many :project_authorizations, dependent: :destroy
+ has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source
alias_method :members, :project_members
has_many :users, through: :project_members
@@ -158,18 +168,20 @@ class Project < ActiveRecord::Base
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
delegate :add_user, to: :team
+ delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
# Validations
validates :creator, presence: true, on: :create
validates :description, length: { maximum: 2000 }, allow_blank: true
validates :name,
presence: true,
- length: { within: 0..255 },
+ length: { maximum: 255 },
format: { with: Gitlab::Regex.project_name_regex,
message: Gitlab::Regex.project_name_regex_message }
validates :path,
presence: true,
- length: { within: 0..255 },
+ project_path: true,
+ length: { maximum: 255 },
format: { with: Gitlab::Regex.project_path_regex,
message: Gitlab::Regex.project_path_regex_message }
validates :namespace, presence: true
@@ -207,8 +219,38 @@ class Project < ActiveRecord::Base
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
- scope :with_builds_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0') }
- scope :with_issues_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.issues_access_level IS NULL or project_features.issues_access_level > 0') }
+ scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
+
+ # "enabled" here means "not disabled". It includes private features!
+ scope :with_feature_enabled, ->(feature) {
+ access_level_attribute = ProjectFeature.access_level_attribute(feature)
+ with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED] })
+ }
+
+ # Picks a feature where the level is exactly that given.
+ scope :with_feature_access_level, ->(feature, level) {
+ access_level_attribute = ProjectFeature.access_level_attribute(feature)
+ with_project_feature.where(project_features: { access_level_attribute => level })
+ }
+
+ scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
+ scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
+
+ # project features may be "disabled", "internal" or "enabled". If "internal",
+ # they are only available to team members. This scope returns projects where
+ # the feature is either enabled, or internal with permission for the user.
+ def self.with_feature_available_for_user(feature, user)
+ return with_feature_enabled(feature) if user.try(:admin?)
+
+ unconditional = with_feature_access_level(feature, [nil, ProjectFeature::ENABLED])
+ return unconditional if user.nil?
+
+ conditional = with_feature_access_level(feature, ProjectFeature::PRIVATE)
+ authorized = user.authorized_projects.merge(conditional.reorder(nil))
+
+ union = Gitlab::SQL::Union.new([unconditional.select(:id), authorized.select(:id)])
+ where(arel_table[:id].in(Arel::Nodes::SqlLiteral.new(union.to_sql)))
+ end
scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') }
scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
@@ -285,87 +327,6 @@ class Project < ActiveRecord::Base
non_archived.where(table[:name].matches(pattern))
end
- # Finds a single project for the given path.
- #
- # path - The full project path (including namespace path).
- #
- # Returns a Project, or nil if no project could be found.
- def find_with_namespace(path)
- namespace_path, project_path = path.split('/', 2)
-
- return unless namespace_path && project_path
-
- namespace_path = connection.quote(namespace_path)
- project_path = connection.quote(project_path)
-
- # On MySQL we want to ensure the ORDER BY uses a case-sensitive match so
- # any literal matches come first, for this we have to use "BINARY".
- # Without this there's still no guarantee in what order MySQL will return
- # rows.
- binary = Gitlab::Database.mysql? ? 'BINARY' : ''
-
- order_sql = "(CASE WHEN #{binary} namespaces.path = #{namespace_path} " \
- "AND #{binary} projects.path = #{project_path} THEN 0 ELSE 1 END)"
-
- where_paths_in([path]).reorder(order_sql).take
- end
-
- # Builds a relation to find multiple projects by their full paths.
- #
- # Each path must be in the following format:
- #
- # namespace_path/project_path
- #
- # For example:
- #
- # gitlab-org/gitlab-ce
- #
- # Usage:
- #
- # Project.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee})
- #
- # This would return the projects with the full paths matching the values
- # given.
- #
- # paths - An Array of full paths (namespace path + project path) for which
- # to find the projects.
- #
- # Returns an ActiveRecord::Relation.
- def where_paths_in(paths)
- wheres = []
- cast_lower = Gitlab::Database.postgresql?
-
- paths.each do |path|
- namespace_path, project_path = path.split('/', 2)
-
- next unless namespace_path && project_path
-
- namespace_path = connection.quote(namespace_path)
- project_path = connection.quote(project_path)
-
- where = "(namespaces.path = #{namespace_path}
- AND projects.path = #{project_path})"
-
- if cast_lower
- where = "(
- #{where}
- OR (
- LOWER(namespaces.path) = LOWER(#{namespace_path})
- AND LOWER(projects.path) = LOWER(#{project_path})
- )
- )"
- end
-
- wheres << where
- end
-
- if wheres.empty?
- none
- else
- joins(:namespace).where(wheres.join(' OR '))
- end
- end
-
def visibility_levels
Gitlab::VisibilityLevel.options
end
@@ -380,7 +341,11 @@ class Project < ActiveRecord::Base
def reference_pattern
name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR
- %r{(?<project>#{name_pattern}/#{name_pattern})}
+
+ %r{
+ ((?<namespace>#{name_pattern})\/)?
+ (?<project>#{name_pattern})
+ }x
end
def trending
@@ -397,6 +362,10 @@ 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?
@@ -611,8 +580,20 @@ class Project < ActiveRecord::Base
end
end
- def to_reference(_from_project = nil)
- path_with_namespace
+ def to_reference(from_project = nil)
+ if cross_namespace_reference?(from_project)
+ path_with_namespace
+ elsif cross_project_reference?(from_project)
+ path
+ end
+ end
+
+ def to_human_reference(from_project = nil)
+ if cross_namespace_reference?(from_project)
+ name_with_namespace
+ elsif cross_project_reference?(from_project)
+ name
+ end
end
def web_url
@@ -648,9 +629,9 @@ class Project < ActiveRecord::Base
self.id
end
- def get_issue(issue_id)
+ def get_issue(issue_id, current_user)
if default_issues_tracker?
- issues.find_by(iid: issue_id)
+ IssuesFinder.new(current_user, project_id: id).find_by(iid: issue_id)
else
ExternalIssue.new(issue_id, self)
end
@@ -718,27 +699,32 @@ class Project < ActiveRecord::Base
update_column(:has_external_wiki, services.external_wikis.any?)
end
- def build_missing_services
+ def find_or_initialize_services
services_templates = Service.where(template: true)
- Service.available_services_names.each do |service_name|
+ Service.available_services_names.map do |service_name|
service = find_service(services, service_name)
- # If service is available but missing in db
- if service.nil?
+ if service
+ service
+ else
# We should check if template for the service exists
template = find_service(services_templates, service_name)
if template.nil?
- # If no template, we should create an instance. Ex `create_gitlab_ci_service`
- public_send("create_#{service_name}_service")
+ # If no template, we should create an instance. Ex `build_gitlab_ci_service`
+ public_send("build_#{service_name}_service")
else
- Service.create_from_template(self.id, template)
+ Service.build_from_template(id, template)
end
end
end
end
+ def find_or_initialize_service(name)
+ find_or_initialize_services.find { |service| service.to_param == name }
+ end
+
def create_labels
Label.templates.each do |label|
params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
@@ -758,6 +744,14 @@ class Project < ActiveRecord::Base
@ci_service ||= ci_services.reorder(nil).find_by(active: true)
end
+ def deployment_services
+ services.where(category: :deployment)
+ end
+
+ def deployment_service
+ @deployment_service ||= deployment_services.reorder(nil).find_by(active: true)
+ end
+
def jira_tracker?
issues_tracker.to_param == 'jira'
end
@@ -819,13 +813,14 @@ class Project < ActiveRecord::Base
end
alias_method :human_name, :name_with_namespace
- def path_with_namespace
- if namespace
- namespace.path + '/' + path
+ 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|
@@ -848,7 +843,7 @@ class Project < ActiveRecord::Base
end
def empty_repo?
- !repository.exists? || !repository.has_visible_content?
+ repository.empty_repo?
end
def repo
@@ -932,7 +927,6 @@ class Project < ActiveRecord::Base
begin
gitlab_shell.mv_repository(repository_storage_path, "#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki")
send_move_instructions(old_path_with_namespace)
- reset_events_cache
@old_path_with_namespace = old_path_with_namespace
@@ -999,22 +993,6 @@ class Project < ActiveRecord::Base
attrs
end
- # Reset events cache related to this project
- #
- # Since we do cache @event we need to reset cache in special cases:
- # * when project was moved
- # * when project was renamed
- # * when the project avatar changes
- # Events cache stored like events/23-20130109142513.
- # The cache key includes updated_at timestamp.
- # Thus it will automatically generate a new fragment
- # when the event is updated because the key changes.
- def reset_events_cache
- Event.where(project_id: self.id).
- order('id DESC').limit(100).
- update_all(updated_at: Time.now)
- end
-
def project_member(user)
project_members.find_by(user_id: user)
end
@@ -1046,7 +1024,7 @@ class Project < ActiveRecord::Base
"refs/heads/#{branch}",
force: true)
repository.copy_gitattributes(branch)
- repository.expire_avatar_cache(branch)
+ repository.expire_avatar_cache
reload_default_branch
end
@@ -1252,18 +1230,10 @@ class Project < ActiveRecord::Base
end
end
- # Checks if `user` is authorized for this project, with at least the
- # `min_access_level` (if given).
- #
- # If you change the logic of this method, please also update `User#authorized_projects`
- def authorized_for_user?(user, min_access_level = nil)
- return false unless user
+ def deployment_variables
+ return [] unless deployment_service
- return true if personal? && namespace_id == user.namespace_id
-
- authorized_for_user_by_group?(user, min_access_level) ||
- authorized_for_user_by_members?(user, min_access_level) ||
- authorized_for_user_by_shared_projects?(user, min_access_level)
+ deployment_service.predefined_variables
end
def append_or_update_attribute(name, value)
@@ -1288,57 +1258,52 @@ class Project < ActiveRecord::Base
Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) }
end
- def environments_for(ref, commit, with_tags: false)
- environment_ids = deployments.group(:environment_id).
- select(:environment_id)
+ def environments_for(ref, commit: nil, with_tags: false)
+ deployments_query = with_tags ? 'ref = ? OR tag IS TRUE' : 'ref = ?'
- environment_ids =
- if with_tags
- environment_ids.where('ref=? OR tag IS TRUE', ref)
- else
- environment_ids.where(ref: ref)
- end
+ environment_ids = deployments
+ .where(deployments_query, ref.to_s)
+ .group(:environment_id)
+ .select(:environment_id)
+
+ environments_found = environments.available
+ .where(id: environment_ids).to_a
+
+ return environments_found unless commit
- environments.available.where(id: environment_ids).select do |environment|
+ environments_found.select do |environment|
environment.includes_commit?(commit)
end
end
+ def environments_recently_updated_on_branch(branch)
+ environments_for(branch).select do |environment|
+ environment.recently_updated_on_branch?(branch)
+ end
+ end
+
private
+ # 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
+ 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
end
- def authorized_for_user_by_group?(user, min_access_level)
- member = user.group_members.find_by(source_id: group)
-
- member && (!min_access_level || member.access_level >= min_access_level)
- end
-
- def authorized_for_user_by_members?(user, min_access_level)
- member = members.find_by(user_id: user)
-
- member && (!min_access_level || member.access_level >= min_access_level)
- end
-
- def authorized_for_user_by_shared_projects?(user, min_access_level)
- shared_projects = user.group_members.joins(group: :shared_projects).
- where(project_group_links: { project_id: self })
-
- if min_access_level
- members_scope = { access_level: Gitlab::Access.values.select { |access| access >= min_access_level } }
- shared_projects = shared_projects.where(members: members_scope)
- end
-
- shared_projects.any?
- end
-
# Similar to the normal callbacks that hook into the life cycle of an
# Active Record object, you can also define callbacks that get triggered
# when you add an object to an association collection. If any of these
@@ -1349,4 +1314,8 @@ class Project < ActiveRecord::Base
def validate_board_limit(board)
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
end
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
new file mode 100644
index 00000000000..a00d43773d9
--- /dev/null
+++ b/app/models/project_authorization.rb
@@ -0,0 +1,8 @@
+class ProjectAuthorization < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :project
+
+ validates :project, presence: true
+ validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
+ validates :user, uniqueness: { scope: [:project, :access_level] }, presence: true
+end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index b37ce1d3cf6..03194fc2141 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -20,6 +20,15 @@ class ProjectFeature < ActiveRecord::Base
FEATURES = %i(issues merge_requests wiki snippets builds repository)
+ class << self
+ def access_level_attribute(feature)
+ feature = feature.model_name.plural.to_sym if feature.respond_to?(:model_name)
+ raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature)
+
+ "#{feature}_access_level".to_sym
+ end
+ end
+
# Default scopes force us to unscope here since a service may need to check
# permissions for a project in pending_delete
# http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
@@ -35,29 +44,26 @@ class ProjectFeature < ActiveRecord::Base
default_value_for :repository_access_level, value: ENABLED, allows_nil: false
def feature_available?(feature, user)
- raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature)
-
- get_permission(user, public_send("#{feature}_access_level"))
+ access_level = public_send(ProjectFeature.access_level_attribute(feature))
+ get_permission(user, access_level)
end
def builds_enabled?
- return true unless builds_access_level
-
builds_access_level > DISABLED
end
def wiki_enabled?
- return true unless wiki_access_level
-
wiki_access_level > DISABLED
end
def merge_requests_enabled?
- return true unless merge_requests_access_level
-
merge_requests_access_level > DISABLED
end
+ def issues_enabled?
+ issues_access_level > DISABLED
+ end
+
private
# Validates builds and merge requests access level
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index db46def11eb..6149c35cc61 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -16,6 +16,9 @@ class ProjectGroupLink < ActiveRecord::Base
validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
validate :different_group
+ after_create :refresh_group_members_authorized_projects
+ after_destroy :refresh_group_members_authorized_projects
+
def self.access_options
Gitlab::Access.options
end
@@ -35,4 +38,8 @@ class ProjectGroupLink < ActiveRecord::Base
errors.add(:base, "Project cannot be shared with the project it is in.")
end
end
+
+ def refresh_group_members_authorized_projects
+ group.refresh_members_authorized_projects
+ end
end
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index 86a06321e21..fe6d7aabb22 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -3,7 +3,8 @@ require "addressable/uri"
class BuildkiteService < CiService
ENDPOINT = "https://buildkite.com"
- prop_accessor :project_url, :token, :enable_ssl_verification
+ prop_accessor :project_url, :token
+ boolean_accessor :enable_ssl_verification
validates :project_url, presence: true, url: true, if: :activated?
validates :token, presence: true, if: :activated?
diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb
new file mode 100644
index 00000000000..a03605d01fb
--- /dev/null
+++ b/app/models/project_services/chat_message/base_message.rb
@@ -0,0 +1,34 @@
+require 'slack-notifier'
+
+module ChatMessage
+ class BaseMessage
+ def initialize(params)
+ raise NotImplementedError
+ end
+
+ def pretext
+ format(message)
+ end
+
+ def fallback
+ end
+
+ def attachments
+ raise NotImplementedError
+ end
+
+ private
+
+ def message
+ raise NotImplementedError
+ end
+
+ def format(string)
+ Slack::Notifier::LinkFormatter.format(string)
+ end
+
+ def attachment_color
+ '#345'
+ end
+ end
+end
diff --git a/app/models/project_services/chat_message/build_message.rb b/app/models/project_services/chat_message/build_message.rb
new file mode 100644
index 00000000000..53e35cb21bf
--- /dev/null
+++ b/app/models/project_services/chat_message/build_message.rb
@@ -0,0 +1,82 @@
+module ChatMessage
+ class BuildMessage < BaseMessage
+ attr_reader :sha
+ attr_reader :ref_type
+ attr_reader :ref
+ attr_reader :status
+ attr_reader :project_name
+ attr_reader :project_url
+ attr_reader :user_name
+ attr_reader :duration
+
+ def initialize(params)
+ @sha = params[:sha]
+ @ref_type = params[:tag] ? 'tag' : 'branch'
+ @ref = params[:ref]
+ @project_name = params[:project_name]
+ @project_url = params[:project_url]
+ @status = params[:commit][:status]
+ @user_name = params[:commit][:author_name]
+ @duration = params[:commit][:duration]
+ end
+
+ def pretext
+ ''
+ end
+
+ def fallback
+ format(message)
+ end
+
+ def attachments
+ [{ text: format(message), color: attachment_color }]
+ end
+
+ private
+
+ def message
+ "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}"
+ end
+
+ def format(string)
+ Slack::Notifier::LinkFormatter.format(string)
+ end
+
+ def humanized_status
+ case status
+ when 'success'
+ 'passed'
+ else
+ status
+ end
+ end
+
+ def attachment_color
+ if status == 'success'
+ 'good'
+ else
+ 'danger'
+ end
+ end
+
+ def branch_url
+ "#{project_url}/commits/#{ref}"
+ end
+
+ def branch_link
+ "[#{ref}](#{branch_url})"
+ end
+
+ def project_link
+ "[#{project_name}](#{project_url})"
+ end
+
+ def commit_url
+ "#{project_url}/commit/#{sha}/builds"
+ end
+
+ def commit_link
+ "[#{Commit.truncate_sha(sha)}](#{commit_url})"
+ end
+ end
+end
diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb
new file mode 100644
index 00000000000..14fd64e5332
--- /dev/null
+++ b/app/models/project_services/chat_message/issue_message.rb
@@ -0,0 +1,69 @@
+module ChatMessage
+ class IssueMessage < BaseMessage
+ attr_reader :user_name
+ attr_reader :title
+ attr_reader :project_name
+ attr_reader :project_url
+ attr_reader :issue_iid
+ attr_reader :issue_url
+ attr_reader :action
+ attr_reader :state
+ attr_reader :description
+
+ def initialize(params)
+ @user_name = params[:user][:username]
+ @project_name = params[:project_name]
+ @project_url = params[:project_url]
+
+ obj_attr = params[:object_attributes]
+ obj_attr = HashWithIndifferentAccess.new(obj_attr)
+ @title = obj_attr[:title]
+ @issue_iid = obj_attr[:iid]
+ @issue_url = obj_attr[:url]
+ @action = obj_attr[:action]
+ @state = obj_attr[:state]
+ @description = obj_attr[:description] || ''
+ end
+
+ def attachments
+ return [] unless opened_issue?
+
+ description_message
+ end
+
+ private
+
+ def message
+ case state
+ when "opened"
+ "[#{project_link}] Issue #{state} by #{user_name}"
+ else
+ "[#{project_link}] Issue #{issue_link} #{state} by #{user_name}"
+ end
+ end
+
+ def opened_issue?
+ action == "open"
+ end
+
+ def description_message
+ [{
+ title: issue_title,
+ title_link: issue_url,
+ text: format(description),
+ color: "#C95823" }]
+ end
+
+ def project_link
+ "[#{project_name}](#{project_url})"
+ end
+
+ def issue_link
+ "[#{issue_title}](#{issue_url})"
+ end
+
+ def issue_title
+ "##{issue_iid} #{title}"
+ end
+ end
+end
diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb
new file mode 100644
index 00000000000..ab5e8b24167
--- /dev/null
+++ b/app/models/project_services/chat_message/merge_message.rb
@@ -0,0 +1,60 @@
+module ChatMessage
+ class MergeMessage < BaseMessage
+ attr_reader :user_name
+ attr_reader :project_name
+ attr_reader :project_url
+ attr_reader :merge_request_id
+ attr_reader :source_branch
+ attr_reader :target_branch
+ attr_reader :state
+ attr_reader :title
+
+ def initialize(params)
+ @user_name = params[:user][:username]
+ @project_name = params[:project_name]
+ @project_url = params[:project_url]
+
+ obj_attr = params[:object_attributes]
+ obj_attr = HashWithIndifferentAccess.new(obj_attr)
+ @merge_request_id = obj_attr[:iid]
+ @source_branch = obj_attr[:source_branch]
+ @target_branch = obj_attr[:target_branch]
+ @state = obj_attr[:state]
+ @title = format_title(obj_attr[:title])
+ end
+
+ def pretext
+ format(message)
+ end
+
+ def attachments
+ []
+ end
+
+ private
+
+ def format_title(title)
+ '*' + title.lines.first.chomp + '*'
+ end
+
+ def message
+ merge_request_message
+ end
+
+ def project_link
+ "[#{project_name}](#{project_url})"
+ end
+
+ def merge_request_message
+ "#{user_name} #{state} #{merge_request_link} in #{project_link}: #{title}"
+ end
+
+ def merge_request_link
+ "[merge request !#{merge_request_id}](#{merge_request_url})"
+ end
+
+ def merge_request_url
+ "#{project_url}/merge_requests/#{merge_request_id}"
+ end
+ end
+end
diff --git a/app/models/project_services/chat_message/note_message.rb b/app/models/project_services/chat_message/note_message.rb
new file mode 100644
index 00000000000..ca1d7207034
--- /dev/null
+++ b/app/models/project_services/chat_message/note_message.rb
@@ -0,0 +1,83 @@
+module ChatMessage
+ class NoteMessage < BaseMessage
+ attr_reader :message
+ attr_reader :user_name
+ attr_reader :project_name
+ attr_reader :project_link
+ attr_reader :note
+ attr_reader :note_url
+ attr_reader :title
+
+ def initialize(params)
+ params = HashWithIndifferentAccess.new(params)
+ @user_name = params[:user][:username]
+ @project_name = params[:project_name]
+ @project_url = params[:project_url]
+
+ obj_attr = params[:object_attributes]
+ obj_attr = HashWithIndifferentAccess.new(obj_attr)
+ @note = obj_attr[:note]
+ @note_url = obj_attr[:url]
+ noteable_type = obj_attr[:noteable_type]
+
+ case noteable_type
+ when "Commit"
+ create_commit_note(HashWithIndifferentAccess.new(params[:commit]))
+ when "Issue"
+ create_issue_note(HashWithIndifferentAccess.new(params[:issue]))
+ when "MergeRequest"
+ create_merge_note(HashWithIndifferentAccess.new(params[:merge_request]))
+ when "Snippet"
+ create_snippet_note(HashWithIndifferentAccess.new(params[:snippet]))
+ end
+ end
+
+ def attachments
+ description_message
+ end
+
+ private
+
+ def format_title(title)
+ title.lines.first.chomp
+ end
+
+ def create_commit_note(commit)
+ commit_sha = commit[:id]
+ commit_sha = Commit.truncate_sha(commit_sha)
+ commented_on_message(
+ "commit #{commit_sha}",
+ format_title(commit[:message]))
+ end
+
+ def create_issue_note(issue)
+ commented_on_message(
+ "issue ##{issue[:iid]}",
+ format_title(issue[:title]))
+ end
+
+ def create_merge_note(merge_request)
+ commented_on_message(
+ "merge request !#{merge_request[:iid]}",
+ format_title(merge_request[:title]))
+ end
+
+ def create_snippet_note(snippet)
+ commented_on_message(
+ "snippet ##{snippet[:id]}",
+ format_title(snippet[:title]))
+ end
+
+ def description_message
+ [{ text: format(@note), color: attachment_color }]
+ end
+
+ def project_link
+ "[#{@project_name}](#{@project_url})"
+ end
+
+ def commented_on_message(target, title)
+ @message = "#{@user_name} [commented on #{target}](#{@note_url}) in #{project_link}: *#{title}*"
+ end
+ end
+end
diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb
new file mode 100644
index 00000000000..210027565a8
--- /dev/null
+++ b/app/models/project_services/chat_message/pipeline_message.rb
@@ -0,0 +1,78 @@
+module ChatMessage
+ class PipelineMessage < BaseMessage
+ attr_reader :ref_type, :ref, :status, :project_name, :project_url,
+ :user_name, :duration, :pipeline_id
+
+ def initialize(data)
+ pipeline_attributes = data[:object_attributes]
+ @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+ @ref = pipeline_attributes[:ref]
+ @status = pipeline_attributes[:status]
+ @duration = pipeline_attributes[:duration]
+ @pipeline_id = pipeline_attributes[:id]
+
+ @project_name = data[:project][:path_with_namespace]
+ @project_url = data[:project][:web_url]
+ @user_name = (data[:user] && data[:user][:name]) || 'API'
+ end
+
+ def pretext
+ ''
+ end
+
+ def fallback
+ format(message)
+ end
+
+ def attachments
+ [{ text: format(message), color: attachment_color }]
+ end
+
+ private
+
+ def message
+ "#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}"
+ end
+
+ def format(string)
+ Slack::Notifier::LinkFormatter.format(string)
+ end
+
+ def humanized_status
+ case status
+ when 'success'
+ 'passed'
+ else
+ status
+ end
+ end
+
+ def attachment_color
+ if status == 'success'
+ 'good'
+ else
+ 'danger'
+ end
+ end
+
+ def branch_url
+ "#{project_url}/commits/#{ref}"
+ end
+
+ def branch_link
+ "[#{ref}](#{branch_url})"
+ end
+
+ def project_link
+ "[#{project_name}](#{project_url})"
+ end
+
+ def pipeline_url
+ "#{project_url}/pipelines/#{pipeline_id}"
+ end
+
+ def pipeline_link
+ "[##{pipeline_id}](#{pipeline_url})"
+ end
+ end
+end
diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb
new file mode 100644
index 00000000000..2d73b71ec37
--- /dev/null
+++ b/app/models/project_services/chat_message/push_message.rb
@@ -0,0 +1,110 @@
+module ChatMessage
+ class PushMessage < BaseMessage
+ attr_reader :after
+ attr_reader :before
+ attr_reader :commits
+ attr_reader :project_name
+ attr_reader :project_url
+ attr_reader :ref
+ attr_reader :ref_type
+ attr_reader :user_name
+
+ def initialize(params)
+ @after = params[:after]
+ @before = params[:before]
+ @commits = params.fetch(:commits, [])
+ @project_name = params[:project_name]
+ @project_url = params[:project_url]
+ @ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch'
+ @ref = Gitlab::Git.ref_name(params[:ref])
+ @user_name = params[:user_name]
+ end
+
+ def pretext
+ format(message)
+ end
+
+ def attachments
+ return [] if new_branch? || removed_branch?
+
+ commit_message_attachments
+ end
+
+ private
+
+ def message
+ if new_branch?
+ new_branch_message
+ elsif removed_branch?
+ removed_branch_message
+ else
+ push_message
+ end
+ end
+
+ def format(string)
+ Slack::Notifier::LinkFormatter.format(string)
+ end
+
+ def new_branch_message
+ "#{user_name} pushed new #{ref_type} #{branch_link} to #{project_link}"
+ end
+
+ def removed_branch_message
+ "#{user_name} removed #{ref_type} #{ref} from #{project_link}"
+ end
+
+ def push_message
+ "#{user_name} pushed to #{ref_type} #{branch_link} of #{project_link} (#{compare_link})"
+ end
+
+ def commit_messages
+ commits.map { |commit| compose_commit_message(commit) }.join("\n")
+ end
+
+ def commit_message_attachments
+ [{ text: format(commit_messages), color: attachment_color }]
+ end
+
+ def compose_commit_message(commit)
+ author = commit[:author][:name]
+ id = Commit.truncate_sha(commit[:id])
+ message = commit[:message]
+ url = commit[:url]
+
+ "[#{id}](#{url}): #{message} - #{author}"
+ end
+
+ def new_branch?
+ Gitlab::Git.blank_ref?(before)
+ end
+
+ def removed_branch?
+ Gitlab::Git.blank_ref?(after)
+ end
+
+ def branch_url
+ "#{project_url}/commits/#{ref}"
+ end
+
+ def compare_url
+ "#{project_url}/compare/#{before}...#{after}"
+ end
+
+ def branch_link
+ "[#{ref}](#{branch_url})"
+ end
+
+ def project_link
+ "[#{project_name}](#{project_url})"
+ end
+
+ def compare_link
+ "[Compare changes](#{compare_url})"
+ end
+
+ def attachment_color
+ '#345'
+ end
+ end
+end
diff --git a/app/models/project_services/chat_message/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb
new file mode 100644
index 00000000000..134083e4504
--- /dev/null
+++ b/app/models/project_services/chat_message/wiki_page_message.rb
@@ -0,0 +1,53 @@
+module ChatMessage
+ class WikiPageMessage < BaseMessage
+ attr_reader :user_name
+ attr_reader :title
+ attr_reader :project_name
+ attr_reader :project_url
+ attr_reader :wiki_page_url
+ attr_reader :action
+ attr_reader :description
+
+ def initialize(params)
+ @user_name = params[:user][:username]
+ @project_name = params[:project_name]
+ @project_url = params[:project_url]
+
+ obj_attr = params[:object_attributes]
+ obj_attr = HashWithIndifferentAccess.new(obj_attr)
+ @title = obj_attr[:title]
+ @wiki_page_url = obj_attr[:url]
+ @description = obj_attr[:content]
+
+ @action =
+ case obj_attr[:action]
+ when "create"
+ "created"
+ when "update"
+ "edited"
+ end
+ end
+
+ def attachments
+ description_message
+ end
+
+ private
+
+ def message
+ "#{user_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*"
+ end
+
+ def description_message
+ [{ text: format(@description), color: attachment_color }]
+ end
+
+ def project_link
+ "[#{project_name}](#{project_url})"
+ end
+
+ def wiki_page_link
+ "[wiki page](#{wiki_page_url})"
+ end
+ end
+end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
new file mode 100644
index 00000000000..b0556987721
--- /dev/null
+++ b/app/models/project_services/chat_notification_service.rb
@@ -0,0 +1,147 @@
+# Base class for Chat notifications services
+# This class is not meant to be used directly, but only to inherit from.
+class ChatNotificationService < Service
+ include ChatMessage
+
+ default_value_for :category, 'chat'
+
+ prop_accessor :webhook, :username, :channel
+ boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
+
+ validates :webhook, presence: true, url: true, if: :activated?
+
+ def initialize_properties
+ # Custom serialized properties initialization
+ self.supported_events.each { |event| self.class.prop_accessor(event_channel_name(event)) }
+
+ if properties.nil?
+ self.properties = {}
+ self.notify_only_broken_builds = true
+ self.notify_only_broken_pipelines = true
+ end
+ end
+
+ def can_test?
+ valid?
+ end
+
+ def supported_events
+ %w[push issue confidential_issue merge_request note tag_push
+ build pipeline wiki_page]
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+ return unless webhook.present?
+
+ object_kind = data[:object_kind]
+
+ data = data.merge(
+ project_url: project_url,
+ project_name: project_name
+ )
+
+ # WebHook events often have an 'update' event that follows a 'open' or
+ # 'close' action. Ignore update events for now to prevent duplicate
+ # messages from arriving.
+
+ message = get_message(object_kind, data)
+
+ return false unless message
+
+ opt = {}
+
+ opt[:channel] = get_channel_field(object_kind).presence || channel || default_channel
+ opt[:username] = username if username
+ notifier = Slack::Notifier.new(webhook, opt)
+ notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
+
+ true
+ end
+
+ def event_channel_names
+ supported_events.map { |event| event_channel_name(event) }
+ end
+
+ def event_field(event)
+ fields.find { |field| field[:name] == event_channel_name(event) }
+ end
+
+ def global_fields
+ fields.reject { |field| field[:name].end_with?('channel') }
+ end
+
+ def default_channel
+ raise NotImplementedError
+ end
+
+ private
+
+ def get_message(object_kind, data)
+ case object_kind
+ when "push", "tag_push"
+ PushMessage.new(data)
+ when "issue"
+ IssueMessage.new(data) unless is_update?(data)
+ when "merge_request"
+ MergeMessage.new(data) unless is_update?(data)
+ when "note"
+ NoteMessage.new(data)
+ when "build"
+ BuildMessage.new(data) if should_build_be_notified?(data)
+ when "pipeline"
+ PipelineMessage.new(data) if should_pipeline_be_notified?(data)
+ when "wiki_page"
+ WikiPageMessage.new(data)
+ end
+ end
+
+ def get_channel_field(event)
+ field_name = event_channel_name(event)
+ self.public_send(field_name)
+ end
+
+ def build_event_channels
+ supported_events.reduce([]) do |channels, event|
+ channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel }
+ end
+ end
+
+ def event_channel_name(event)
+ "#{event}_channel"
+ end
+
+ def project_name
+ project.name_with_namespace.gsub(/\s/, '')
+ end
+
+ def project_url
+ project.web_url
+ end
+
+ def is_update?(data)
+ data[:object_attributes][:action] == 'update'
+ end
+
+ def should_build_be_notified?(data)
+ case data[:commit][:status]
+ when 'success'
+ !notify_only_broken_builds?
+ when 'failed'
+ true
+ else
+ false
+ end
+ end
+
+ def should_pipeline_be_notified?(data)
+ case data[:object_attributes][:status]
+ when 'success'
+ !notify_only_broken_pipelines?
+ when 'failed'
+ true
+ else
+ false
+ end
+ end
+end
diff --git a/app/models/project_services/chat_service.rb b/app/models/project_services/chat_service.rb
new file mode 100644
index 00000000000..574788462de
--- /dev/null
+++ b/app/models/project_services/chat_service.rb
@@ -0,0 +1,21 @@
+# Base class for Chat services
+# This class is not meant to be used directly, but only to inherit from.
+class ChatService < Service
+ default_value_for :category, 'chat'
+
+ has_many :chat_names, foreign_key: :service_id
+
+ def valid_token?(token)
+ self.respond_to?(:token) &&
+ self.token.present? &&
+ ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
+ end
+
+ def supported_events
+ []
+ end
+
+ def trigger(params)
+ raise NotImplementedError
+ end
+end
diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb
new file mode 100644
index 00000000000..da6be9dd7b7
--- /dev/null
+++ b/app/models/project_services/deployment_service.rb
@@ -0,0 +1,15 @@
+# Base class for deployment services
+#
+# These services integrate with a deployment solution like Kubernetes/OpenShift,
+# Mesosphere, etc, to provide additional features to environments.
+class DeploymentService < Service
+ default_value_for :category, 'deployment'
+
+ def supported_events
+ []
+ end
+
+ def predefined_variables
+ []
+ end
+end
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 5e4dd101c53..adc78a427ee 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -1,5 +1,6 @@
class DroneCiService < CiService
- prop_accessor :drone_url, :token, :enable_ssl_verification
+ prop_accessor :drone_url, :token
+ boolean_accessor :enable_ssl_verification
validates :drone_url, presence: true, url: true, if: :activated?
validates :token, presence: true, if: :activated?
diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb
index e0083c43adb..79285cbd26d 100644
--- a/app/models/project_services/emails_on_push_service.rb
+++ b/app/models/project_services/emails_on_push_service.rb
@@ -1,6 +1,6 @@
class EmailsOnPushService < Service
- prop_accessor :send_from_committer_email
- prop_accessor :disable_diffs
+ boolean_accessor :send_from_committer_email
+ boolean_accessor :disable_diffs
prop_accessor :recipients
validates :recipients, presence: true, if: :activated?
@@ -24,20 +24,20 @@ class EmailsOnPushService < Service
return unless supported_events.include?(push_data[:object_kind])
EmailsOnPushWorker.perform_async(
- project_id,
- recipients,
- push_data,
- send_from_committer_email: send_from_committer_email?,
- disable_diffs: disable_diffs?
+ project_id,
+ recipients,
+ push_data,
+ send_from_committer_email: send_from_committer_email?,
+ disable_diffs: disable_diffs?
)
end
def send_from_committer_email?
- self.send_from_committer_email == "1"
+ Gitlab::Utils.to_boolean(self.send_from_committer_email)
end
def disable_diffs?
- self.disable_diffs == "1"
+ Gitlab::Utils.to_boolean(self.disable_diffs)
end
def fields
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 660a8ae3421..915f6fed74c 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -8,8 +8,8 @@ class HipchatService < Service
ul ol li dl dt dd
]
- prop_accessor :token, :room, :server, :notify, :color, :api_version
- boolean_accessor :notify_only_broken_builds
+ prop_accessor :token, :room, :server, :color, :api_version
+ boolean_accessor :notify_only_broken_builds, :notify
validates :token, presence: true, if: :activated?
def initialize_properties
@@ -75,7 +75,7 @@ class HipchatService < Service
end
def message_options(data = nil)
- { notify: notify.present? && notify == '1', color: message_color(data) }
+ { notify: notify.present? && Gitlab::Utils.to_boolean(notify), color: message_color(data) }
end
def create_message(data)
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index ce7d1c5d5b1..7355918feab 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -2,7 +2,8 @@ require 'uri'
class IrkerService < Service
prop_accessor :server_host, :server_port, :default_irc_uri
- prop_accessor :colorize_messages, :recipients, :channels
+ prop_accessor :recipients, :channels
+ boolean_accessor :colorize_messages
validates :recipients, presence: true, if: :activated?
before_validation :get_channels
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 207bb816ad1..bce2cdd5516 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -85,8 +85,8 @@ class IssueTrackerService < Service
def enabled_in_gitlab_config
Gitlab.config.issues_tracker &&
- Gitlab.config.issues_tracker.values.any? &&
- issues_tracker
+ Gitlab.config.issues_tracker.values.any? &&
+ issues_tracker
end
def issues_tracker
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 2dbe0075465..2d969d2fcb6 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -1,24 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-# build_events :boolean default(FALSE), not null
-#
-
class JiraService < IssueTrackerService
include Gitlab::Routing.url_helpers
@@ -30,6 +9,13 @@ class JiraService < IssueTrackerService
before_update :reset_password
+ # This is confusing, but JiraService does not really support these events.
+ # The values here are required to display correct options in the service
+ # configuration screen.
+ def supported_events
+ %w(commit merge_request)
+ end
+
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
def reference_pattern
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
@@ -70,13 +56,13 @@ class JiraService < IssueTrackerService
end
def jira_project
- @jira_project ||= client.Project.find(project_key)
+ @jira_project ||= jira_request { client.Project.find(project_key) }
end
def help
- 'See the ' \
- '[integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) '\
- 'for 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).'
end
def title
@@ -122,27 +108,43 @@ class JiraService < IssueTrackerService
"#{url}/secure/CreateIssue.jspa"
end
- def execute(push, issue = nil)
- if issue.nil?
- # No specific issue, that means
- # we just want to test settings
- test_settings
- else
- close_issue(push, issue)
- end
+ def execute(push)
+ # This method is a no-op, because currently JiraService does not
+ # support any events.
+ end
+
+ def close_issue(entity, external_issue)
+ issue = jira_request { client.Issue.find(external_issue.iid) }
+
+ return if issue.nil? || issue.resolution.present? || !jira_issue_transition_id.present?
+
+ commit_id = if entity.is_a?(Commit)
+ entity.id
+ elsif entity.is_a?(MergeRequest)
+ entity.diff_head_sha
+ end
+
+ commit_url = build_entity_url(:commit, commit_id)
+
+ # Depending on the JIRA project's workflow, a comment during transition
+ # may or may not be allowed. Refresh the issue after transition and check
+ # if it is closed, so we don't have one comment for every commit.
+ issue = jira_request { client.Issue.find(issue.key) } if transition_issue(issue)
+ add_issue_solved_comment(issue, commit_id, commit_url) if issue.resolution
end
def create_cross_reference_note(mentioned, noteable, author)
- issue_key = mentioned.id
- project = self.project
- noteable_name = noteable.class.name.underscore.downcase
- noteable_id = if noteable.is_a?(Commit)
- noteable.id
- else
- noteable.iid
- end
+ unless can_cross_reference?(noteable)
+ return "Events for #{noteable.model_name.plural.humanize(capitalize: false)} are disabled."
+ end
+
+ jira_issue = jira_request { client.Issue.find(mentioned.id) }
+
+ return unless jira_issue.present?
- entity_url = build_entity_url(noteable_name.to_sym, noteable_id)
+ noteable_id = noteable.respond_to?(:iid) ? noteable.iid : noteable.id
+ noteable_type = noteable_name(noteable)
+ entity_url = build_entity_url(noteable_type, noteable_id)
data = {
user: {
@@ -150,17 +152,17 @@ class JiraService < IssueTrackerService
url: resource_url(user_path(author)),
},
project: {
- name: project.path_with_namespace,
- url: resource_url(namespace_project_path(project.namespace, project))
+ name: self.project.path_with_namespace,
+ url: resource_url(namespace_project_path(project.namespace, self.project))
},
entity: {
- name: noteable_name.humanize.downcase,
+ name: noteable_type.humanize.downcase,
url: entity_url,
title: noteable.title
}
}
- add_comment(data, issue_key)
+ add_comment(data, jira_issue)
end
# reason why service cannot be tested
@@ -168,6 +170,11 @@ class JiraService < IssueTrackerService
"Please fill in Password and Username."
end
+ def test(_)
+ result = test_settings
+ { success: result.present?, result: result }
+ end
+
def can_test?
username.present? && password.present?
end
@@ -181,91 +188,124 @@ class JiraService < IssueTrackerService
def test_settings
return unless url.present?
# Test settings by getting the project
- jira_project
-
- rescue Errno::ECONNREFUSED, JIRA::HTTPError => e
- Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{url}."
- false
+ jira_request { jira_project.present? }
end
private
- def close_issue(entity, issue)
- commit_id = if entity.is_a?(Commit)
- entity.id
- elsif entity.is_a?(MergeRequest)
- entity.diff_head_sha
- end
-
- commit_url = build_entity_url(:commit, commit_id)
-
- # Depending on the JIRA project's workflow, a comment during transition
- # may or may not be allowed. Split the operation in to two calls so the
- # comment always works.
- transition_issue(issue)
- add_issue_solved_comment(issue, commit_id, commit_url)
+ def can_cross_reference?(noteable)
+ case noteable
+ when Commit then commit_events
+ when MergeRequest then merge_requests_events
+ else true
+ end
end
def transition_issue(issue)
- issue = client.Issue.find(issue.iid)
issue.transitions.build.save(transition: { id: jira_issue_transition_id })
end
def add_issue_solved_comment(issue, commit_id, commit_url)
- comment = "Issue solved with [#{commit_id}|#{commit_url}]."
- send_message(issue.iid, comment)
+ link_title = "GitLab: Solved by commit #{commit_id}."
+ comment = "Issue solved with [#{commit_id}|#{commit_url}]."
+ link_props = build_remote_link_props(url: commit_url, title: link_title, resolved: true)
+ send_message(issue, comment, link_props)
end
- def add_comment(data, issue_key)
- user_name = data[:user][:name]
- user_url = data[:user][:url]
- entity_name = data[:entity][:name]
- entity_url = data[:entity][:url]
+ def add_comment(data, issue)
+ user_name = data[:user][:name]
+ user_url = data[:user][:url]
+ entity_name = data[:entity][:name]
+ entity_url = data[:entity][:url]
entity_title = data[:entity][:title]
project_name = data[:project][:name]
- message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'"
+ message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title.chomp}'"
+ link_title = "GitLab: Mentioned on #{entity_name} - #{entity_title}"
+ link_props = build_remote_link_props(url: entity_url, title: link_title)
- unless comment_exists?(issue_key, message)
- send_message(issue_key, message)
+ unless comment_exists?(issue, message)
+ send_message(issue, message, link_props)
end
end
- def comment_exists?(issue_key, message)
- comments = client.Issue.find(issue_key).comments
- comments.map { |comment| comment.body.include?(message) }.any?
+ def comment_exists?(issue, message)
+ comments = jira_request { issue.comments }
+
+ comments.present? && comments.any? { |comment| comment.body.include?(message) }
end
- def send_message(issue_key, message)
+ def send_message(issue, message, remote_link_props)
return unless url.present?
- issue = client.Issue.find(issue_key)
+ jira_request do
+ if issue.comments.build.save!(body: message)
+ remote_link = issue.remotelink.build
+ remote_link.save!(remote_link_props)
+ result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}."
+ end
- if issue.comments.build.save!(body: message)
- result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}."
+ Rails.logger.info(result_message)
+ result_message
end
+ end
- Rails.logger.info(result_message)
- result_message
- rescue URI::InvalidURIError, Errno::ECONNREFUSED, JIRA::HTTPError => e
- Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}"
+ # Build remote link on JIRA properties
+ # Icons here must be available on WEB so JIRA can read the URL
+ # We are using a open word graphics icon which have LGPL license
+ def build_remote_link_props(url:, title:, resolved: false)
+ status = {
+ resolved: resolved
+ }
+
+ if resolved
+ status[:icon] = {
+ title: 'Closed',
+ url16x16: 'http://www.openwebgraphics.com/resources/data/1768/16x16_apply.png'
+ }
+ end
+
+ {
+ GlobalID: 'GitLab',
+ object: {
+ url: url,
+ title: title,
+ status: status,
+ icon: { title: 'GitLab', url16x16: 'https://gitlab.com/favicon.ico' }
+ }
+ }
end
def resource_url(resource)
"#{Settings.gitlab.base_url.chomp("/")}#{resource}"
end
- def build_entity_url(entity_name, entity_id)
- resource_url(
- polymorphic_url(
- [
- self.project.namespace.becomes(Namespace),
- self.project,
- entity_name
- ],
- id: entity_id,
- routing_type: :path
- )
+ def build_entity_url(noteable_type, entity_id)
+ polymorphic_url(
+ [
+ self.project.namespace.becomes(Namespace),
+ self.project,
+ noteable_type.to_sym
+ ],
+ id: entity_id,
+ host: Settings.gitlab.base_url
)
end
+
+ def noteable_name(noteable)
+ name = noteable.model_name.singular
+
+ # ProjectSnippet inherits from Snippet class so it causes
+ # routing error building the URL.
+ name == "project_snippet" ? "snippet" : name
+ end
+
+ # Handle errors when doing JIRA API calls
+ def jira_request
+ yield
+
+ rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError => e
+ Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}"
+ nil
+ end
end
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
new file mode 100644
index 00000000000..f5fbf8b353b
--- /dev/null
+++ b/app/models/project_services/kubernetes_service.rb
@@ -0,0 +1,128 @@
+class KubernetesService < DeploymentService
+ # Namespace defaults to the project path, but can be overridden in case that
+ # is an invalid or inappropriate name
+ prop_accessor :namespace
+
+ # Access to kubernetes is directly through the API
+ prop_accessor :api_url
+
+ # Bearer authentication
+ # TODO: user/password auth, client certificates
+ prop_accessor :token
+
+ # Provide a custom CA bundle for self-signed deployments
+ prop_accessor :ca_pem
+
+ with_options presence: true, if: :activated? do
+ validates :api_url, url: true
+ validates :token
+
+ validates :namespace,
+ format: {
+ with: Gitlab::Regex.kubernetes_namespace_regex,
+ message: Gitlab::Regex.kubernetes_namespace_regex_message,
+ },
+ length: 1..63
+ end
+
+ def initialize_properties
+ if properties.nil?
+ self.properties = {}
+ self.namespace = project.path if project.present?
+ end
+ end
+
+ def title
+ 'Kubernetes'
+ end
+
+ def description
+ 'Kubernetes / Openshift integration'
+ end
+
+ def help
+ ''
+ end
+
+ def to_param
+ 'kubernetes'
+ end
+
+ def fields
+ [
+ { type: 'text',
+ name: 'namespace',
+ title: 'Kubernetes namespace',
+ placeholder: 'Kubernetes namespace',
+ },
+ { type: 'text',
+ name: 'api_url',
+ title: 'API URL',
+ placeholder: 'Kubernetes API URL, like https://kube.example.com/',
+ },
+ { type: 'text',
+ name: 'token',
+ title: 'Service token',
+ placeholder: 'Service token',
+ },
+ { type: 'textarea',
+ name: 'ca_pem',
+ title: 'Custom CA bundle',
+ placeholder: 'Certificate Authority bundle (PEM format)',
+ },
+ ]
+ end
+
+ # Check we can connect to the Kubernetes API
+ def test(*args)
+ kubeclient = build_kubeclient
+ kubeclient.discover
+
+ { success: kubeclient.discovered, result: "Checked API discovery endpoint" }
+ rescue => err
+ { success: false, result: err }
+ end
+
+ def predefined_variables
+ variables = [
+ { key: 'KUBE_URL', value: api_url, public: true },
+ { key: 'KUBE_TOKEN', value: token, public: false },
+ { key: 'KUBE_NAMESPACE', value: namespace, public: true }
+ ]
+ variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } if ca_pem.present?
+ variables
+ end
+
+ private
+
+ def build_kubeclient(api_path = '/api', api_version = 'v1')
+ return nil unless api_url && namespace && token
+
+ url = URI.parse(api_url)
+ url.path = url.path[0..-2] if url.path[-1] == "/"
+ url.path += api_path
+
+ ::Kubeclient::Client.new(
+ url,
+ api_version,
+ ssl_options: kubeclient_ssl_options,
+ auth_options: kubeclient_auth_options,
+ http_proxy_uri: ENV['http_proxy']
+ )
+ end
+
+ def kubeclient_ssl_options
+ opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER }
+
+ if ca_pem.present?
+ opts[:cert_store] = OpenSSL::X509::Store.new
+ opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem))
+ end
+
+ opts
+ end
+
+ def kubeclient_auth_options
+ { bearer_token: token }
+ end
+end
diff --git a/app/models/project_services/mattermost_notification_service.rb b/app/models/project_services/mattermost_notification_service.rb
new file mode 100644
index 00000000000..de18c4b1f00
--- /dev/null
+++ b/app/models/project_services/mattermost_notification_service.rb
@@ -0,0 +1,41 @@
+class MattermostNotificationService < ChatNotificationService
+ def title
+ 'Mattermost notifications'
+ end
+
+ def description
+ 'Receive event notifications in Mattermost'
+ end
+
+ def to_param
+ 'mattermost_notification'
+ end
+
+ def help
+ 'This service sends notifications about projects events to Mattermost channels.<br />
+ To set up this service:
+ <ol>
+ <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation. </li>
+ <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event. </li>
+ <li>Paste the webhook <strong>URL</strong> into the field bellow. </li>
+ <li>Select events below to enable notifications. The channel and username are optional. </li>
+ </ol>'
+ end
+
+ def fields
+ default_fields + build_event_channels
+ end
+
+ def default_fields
+ [
+ { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' },
+ { type: 'text', name: 'username', placeholder: 'username' },
+ { type: 'checkbox', name: 'notify_only_broken_builds' },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' },
+ ]
+ end
+
+ def default_channel
+ "#town-square"
+ end
+end
diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb
new file mode 100644
index 00000000000..33431f41dc2
--- /dev/null
+++ b/app/models/project_services/mattermost_slash_commands_service.rb
@@ -0,0 +1,49 @@
+class MattermostSlashCommandsService < ChatService
+ include TriggersHelper
+
+ prop_accessor :token
+
+ def can_test?
+ false
+ end
+
+ def title
+ 'Mattermost Command'
+ end
+
+ def description
+ "Perform common operations on GitLab in Mattermost"
+ end
+
+ def to_param
+ 'mattermost_slash_commands'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'token', placeholder: '' }
+ ]
+ end
+
+ def trigger(params)
+ return nil unless valid_token?(params[:token])
+
+ user = find_chat_user(params)
+ unless user
+ url = authorize_chat_name_url(params)
+ return Mattermost::Presenter.authorize_chat_name(url)
+ end
+
+ Gitlab::ChatCommands::Command.new(project, user, params).execute
+ end
+
+ private
+
+ def find_chat_user(params)
+ ChatNames::FindUserService.new(self, params).execute
+ end
+
+ def authorize_chat_name_url(params)
+ ChatNames::AuthorizeUserService.new(self, params).execute
+ end
+end
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index ec3c1bc85ee..745f9bd1b43 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -1,10 +1,7 @@
class PipelinesEmailService < Service
prop_accessor :recipients
- boolean_accessor :add_pusher
boolean_accessor :notify_only_broken_pipelines
- validates :recipients,
- presence: true,
- if: ->(s) { s.activated? && !s.add_pusher? }
+ validates :recipients, presence: true, if: :activated?
def initialize_properties
self.properties ||= { notify_only_broken_pipelines: true }
@@ -34,8 +31,8 @@ class PipelinesEmailService < Service
return unless all_recipients.any?
- pipeline = Ci::Pipeline.find(data[:object_attributes][:id])
- Ci::SendPipelineNotificationService.new(pipeline).execute(all_recipients)
+ pipeline_id = data[:object_attributes][:id]
+ PipelineNotificationWorker.new.perform(pipeline_id, all_recipients)
end
def can_test?
@@ -58,9 +55,6 @@ class PipelinesEmailService < Service
name: 'recipients',
placeholder: 'Emails separated by comma' },
{ type: 'checkbox',
- name: 'add_pusher',
- label: 'Add pusher to recipients list' },
- { type: 'checkbox',
name: 'notify_only_broken_pipelines' },
]
end
@@ -85,12 +79,6 @@ class PipelinesEmailService < Service
end
def retrieve_recipients(data)
- all_recipients = recipients.to_s.split(',').reject(&:blank?)
-
- if add_pusher? && data[:user].try(:[], :email)
- all_recipients << data[:user][:email]
- end
-
- all_recipients
+ recipients.to_s.split(',').reject(&:blank?)
end
end
diff --git a/app/models/project_services/slack_notification_service.rb b/app/models/project_services/slack_notification_service.rb
new file mode 100644
index 00000000000..3cbf89efba4
--- /dev/null
+++ b/app/models/project_services/slack_notification_service.rb
@@ -0,0 +1,40 @@
+class SlackNotificationService < ChatNotificationService
+ def title
+ 'Slack notifications'
+ end
+
+ def description
+ 'Receive event notifications in Slack'
+ end
+
+ def to_param
+ 'slack_notification'
+ end
+
+ def help
+ 'This service sends notifications about projects events to Slack channels.<br />
+ To setup this service:
+ <ol>
+ <li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event. </li>
+ <li>Paste the <strong>Webhook URL</strong> into the field below. </li>
+ <li>Select events below to enable notifications. The channel and username are optional. </li>
+ </ol>'
+ end
+
+ def fields
+ default_fields + build_event_channels
+ end
+
+ def default_fields
+ [
+ { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' },
+ { type: 'text', name: 'username', placeholder: 'username' },
+ { type: 'checkbox', name: 'notify_only_broken_builds' },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' },
+ ]
+ end
+
+ def default_channel
+ "#general"
+ end
+end
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
deleted file mode 100644
index e1b937817f4..00000000000
--- a/app/models/project_services/slack_service.rb
+++ /dev/null
@@ -1,176 +0,0 @@
-class SlackService < Service
- prop_accessor :webhook, :username, :channel
- boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
- validates :webhook, presence: true, url: true, if: :activated?
-
- def initialize_properties
- # Custom serialized properties initialization
- self.supported_events.each { |event| self.class.prop_accessor(event_channel_name(event)) }
-
- if properties.nil?
- self.properties = {}
- self.notify_only_broken_builds = true
- self.notify_only_broken_pipelines = true
- end
- end
-
- def title
- 'Slack'
- end
-
- def description
- 'A team communication tool for the 21st century'
- end
-
- def to_param
- 'slack'
- end
-
- def help
- 'This service sends notifications to your Slack channel.<br/>
- To setup this Service you need to create a new <b>"Incoming webhook"</b> in your Slack integration panel,
- and enter the Webhook URL below.'
- end
-
- def fields
- default_fields =
- [
- { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' },
- { type: 'text', name: 'username', placeholder: 'username' },
- { type: 'text', name: 'channel', placeholder: "#general" },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
- { type: 'checkbox', name: 'notify_only_broken_pipelines' },
- ]
-
- default_fields + build_event_channels
- end
-
- def supported_events
- %w[push issue confidential_issue merge_request note tag_push
- build pipeline wiki_page]
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
- return unless webhook.present?
-
- object_kind = data[:object_kind]
-
- data = data.merge(
- project_url: project_url,
- project_name: project_name
- )
-
- # WebHook events often have an 'update' event that follows a 'open' or
- # 'close' action. Ignore update events for now to prevent duplicate
- # messages from arriving.
-
- message = get_message(object_kind, data)
-
- if message
- opt = {}
-
- event_channel = get_channel_field(object_kind) || channel
-
- opt[:channel] = event_channel if event_channel
- opt[:username] = username if username
-
- notifier = Slack::Notifier.new(webhook, opt)
- notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
-
- true
- else
- false
- end
- end
-
- def event_channel_names
- supported_events.map { |event| event_channel_name(event) }
- end
-
- def event_field(event)
- fields.find { |field| field[:name] == event_channel_name(event) }
- end
-
- def global_fields
- fields.reject { |field| field[:name].end_with?('channel') }
- end
-
- private
-
- def get_message(object_kind, data)
- case object_kind
- when "push", "tag_push"
- PushMessage.new(data)
- when "issue"
- IssueMessage.new(data) unless is_update?(data)
- when "merge_request"
- MergeMessage.new(data) unless is_update?(data)
- when "note"
- NoteMessage.new(data)
- when "build"
- BuildMessage.new(data) if should_build_be_notified?(data)
- when "pipeline"
- PipelineMessage.new(data) if should_pipeline_be_notified?(data)
- when "wiki_page"
- WikiPageMessage.new(data)
- end
- end
-
- def get_channel_field(event)
- field_name = event_channel_name(event)
- self.public_send(field_name)
- end
-
- def build_event_channels
- supported_events.reduce([]) do |channels, event|
- channels << { type: 'text', name: event_channel_name(event), placeholder: "#general" }
- end
- end
-
- def event_channel_name(event)
- "#{event}_channel"
- end
-
- def project_name
- project.name_with_namespace.gsub(/\s/, '')
- end
-
- def project_url
- project.web_url
- end
-
- def is_update?(data)
- data[:object_attributes][:action] == 'update'
- end
-
- def should_build_be_notified?(data)
- case data[:commit][:status]
- when 'success'
- !notify_only_broken_builds?
- when 'failed'
- true
- else
- false
- end
- end
-
- def should_pipeline_be_notified?(data)
- case data[:object_attributes][:status]
- when 'success'
- !notify_only_broken_pipelines?
- when 'failed'
- true
- else
- false
- end
- end
-end
-
-require "slack_service/issue_message"
-require "slack_service/push_message"
-require "slack_service/merge_message"
-require "slack_service/note_message"
-require "slack_service/build_message"
-require "slack_service/pipeline_message"
-require "slack_service/wiki_page_message"
diff --git a/app/models/project_services/slack_service/base_message.rb b/app/models/project_services/slack_service/base_message.rb
deleted file mode 100644
index f1182824687..00000000000
--- a/app/models/project_services/slack_service/base_message.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-require 'slack-notifier'
-
-class SlackService
- class BaseMessage
- def initialize(params)
- raise NotImplementedError
- end
-
- def pretext
- format(message)
- end
-
- def fallback
- end
-
- def attachments
- raise NotImplementedError
- end
-
- private
-
- def message
- raise NotImplementedError
- end
-
- def format(string)
- Slack::Notifier::LinkFormatter.format(string)
- end
-
- def attachment_color
- '#345'
- end
- end
-end
diff --git a/app/models/project_services/slack_service/build_message.rb b/app/models/project_services/slack_service/build_message.rb
deleted file mode 100644
index 0fca4267bad..00000000000
--- a/app/models/project_services/slack_service/build_message.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-class SlackService
- class BuildMessage < BaseMessage
- attr_reader :sha
- attr_reader :ref_type
- attr_reader :ref
- attr_reader :status
- attr_reader :project_name
- attr_reader :project_url
- attr_reader :user_name
- attr_reader :duration
-
- def initialize(params)
- @sha = params[:sha]
- @ref_type = params[:tag] ? 'tag' : 'branch'
- @ref = params[:ref]
- @project_name = params[:project_name]
- @project_url = params[:project_url]
- @status = params[:commit][:status]
- @user_name = params[:commit][:author_name]
- @duration = params[:commit][:duration]
- end
-
- def pretext
- ''
- end
-
- def fallback
- format(message)
- end
-
- def attachments
- [{ text: format(message), color: attachment_color }]
- end
-
- private
-
- def message
- "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}"
- end
-
- def format(string)
- Slack::Notifier::LinkFormatter.format(string)
- end
-
- def humanized_status
- case status
- when 'success'
- 'passed'
- else
- status
- end
- end
-
- def attachment_color
- if status == 'success'
- 'good'
- else
- 'danger'
- end
- end
-
- def branch_url
- "#{project_url}/commits/#{ref}"
- end
-
- def branch_link
- "[#{ref}](#{branch_url})"
- end
-
- def project_link
- "[#{project_name}](#{project_url})"
- end
-
- def commit_url
- "#{project_url}/commit/#{sha}/builds"
- end
-
- def commit_link
- "[#{Commit.truncate_sha(sha)}](#{commit_url})"
- end
- end
-end
diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/slack_service/issue_message.rb
deleted file mode 100644
index cd87a79d0c6..00000000000
--- a/app/models/project_services/slack_service/issue_message.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-class SlackService
- class IssueMessage < BaseMessage
- attr_reader :user_name
- attr_reader :title
- attr_reader :project_name
- attr_reader :project_url
- attr_reader :issue_iid
- attr_reader :issue_url
- attr_reader :action
- attr_reader :state
- attr_reader :description
-
- def initialize(params)
- @user_name = params[:user][:username]
- @project_name = params[:project_name]
- @project_url = params[:project_url]
-
- obj_attr = params[:object_attributes]
- obj_attr = HashWithIndifferentAccess.new(obj_attr)
- @title = obj_attr[:title]
- @issue_iid = obj_attr[:iid]
- @issue_url = obj_attr[:url]
- @action = obj_attr[:action]
- @state = obj_attr[:state]
- @description = obj_attr[:description] || ''
- end
-
- def attachments
- return [] unless opened_issue?
-
- description_message
- end
-
- private
-
- def message
- case state
- when "opened"
- "[#{project_link}] Issue #{state} by #{user_name}"
- else
- "[#{project_link}] Issue #{issue_link} #{state} by #{user_name}"
- end
- end
-
- def opened_issue?
- action == "open"
- end
-
- def description_message
- [{
- title: issue_title,
- title_link: issue_url,
- text: format(description),
- color: "#C95823" }]
- end
-
- def project_link
- "[#{project_name}](#{project_url})"
- end
-
- def issue_link
- "[#{issue_title}](#{issue_url})"
- end
-
- def issue_title
- "##{issue_iid} #{title}"
- end
- end
-end
diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/slack_service/merge_message.rb
deleted file mode 100644
index b7615c96068..00000000000
--- a/app/models/project_services/slack_service/merge_message.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-class SlackService
- class MergeMessage < BaseMessage
- attr_reader :user_name
- attr_reader :project_name
- attr_reader :project_url
- attr_reader :merge_request_id
- attr_reader :source_branch
- attr_reader :target_branch
- attr_reader :state
- attr_reader :title
-
- def initialize(params)
- @user_name = params[:user][:username]
- @project_name = params[:project_name]
- @project_url = params[:project_url]
-
- obj_attr = params[:object_attributes]
- obj_attr = HashWithIndifferentAccess.new(obj_attr)
- @merge_request_id = obj_attr[:iid]
- @source_branch = obj_attr[:source_branch]
- @target_branch = obj_attr[:target_branch]
- @state = obj_attr[:state]
- @title = format_title(obj_attr[:title])
- end
-
- def pretext
- format(message)
- end
-
- def attachments
- []
- end
-
- private
-
- def format_title(title)
- '*' + title.lines.first.chomp + '*'
- end
-
- def message
- merge_request_message
- end
-
- def project_link
- "[#{project_name}](#{project_url})"
- end
-
- def merge_request_message
- "#{user_name} #{state} #{merge_request_link} in #{project_link}: #{title}"
- end
-
- def merge_request_link
- "[merge request !#{merge_request_id}](#{merge_request_url})"
- end
-
- def merge_request_url
- "#{project_url}/merge_requests/#{merge_request_id}"
- end
- end
-end
diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb
deleted file mode 100644
index 9e84e90f38c..00000000000
--- a/app/models/project_services/slack_service/note_message.rb
+++ /dev/null
@@ -1,83 +0,0 @@
-class SlackService
- class NoteMessage < BaseMessage
- attr_reader :message
- attr_reader :user_name
- attr_reader :project_name
- attr_reader :project_link
- attr_reader :note
- attr_reader :note_url
- attr_reader :title
-
- def initialize(params)
- params = HashWithIndifferentAccess.new(params)
- @user_name = params[:user][:username]
- @project_name = params[:project_name]
- @project_url = params[:project_url]
-
- obj_attr = params[:object_attributes]
- obj_attr = HashWithIndifferentAccess.new(obj_attr)
- @note = obj_attr[:note]
- @note_url = obj_attr[:url]
- noteable_type = obj_attr[:noteable_type]
-
- case noteable_type
- when "Commit"
- create_commit_note(HashWithIndifferentAccess.new(params[:commit]))
- when "Issue"
- create_issue_note(HashWithIndifferentAccess.new(params[:issue]))
- when "MergeRequest"
- create_merge_note(HashWithIndifferentAccess.new(params[:merge_request]))
- when "Snippet"
- create_snippet_note(HashWithIndifferentAccess.new(params[:snippet]))
- end
- end
-
- def attachments
- description_message
- end
-
- private
-
- def format_title(title)
- title.lines.first.chomp
- end
-
- def create_commit_note(commit)
- commit_sha = commit[:id]
- commit_sha = Commit.truncate_sha(commit_sha)
- commented_on_message(
- "[commit #{commit_sha}](#{@note_url})",
- format_title(commit[:message]))
- end
-
- def create_issue_note(issue)
- commented_on_message(
- "[issue ##{issue[:iid]}](#{@note_url})",
- format_title(issue[:title]))
- end
-
- def create_merge_note(merge_request)
- commented_on_message(
- "[merge request !#{merge_request[:iid]}](#{@note_url})",
- format_title(merge_request[:title]))
- end
-
- def create_snippet_note(snippet)
- commented_on_message(
- "[snippet ##{snippet[:id]}](#{@note_url})",
- format_title(snippet[:title]))
- end
-
- def description_message
- [{ text: format(@note), color: attachment_color }]
- end
-
- def project_link
- "[#{@project_name}](#{@project_url})"
- end
-
- def commented_on_message(target_link, title)
- @message = "#{@user_name} commented on #{target_link} in #{project_link}: *#{title}*"
- end
- end
-end
diff --git a/app/models/project_services/slack_service/pipeline_message.rb b/app/models/project_services/slack_service/pipeline_message.rb
deleted file mode 100644
index f06b3562965..00000000000
--- a/app/models/project_services/slack_service/pipeline_message.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-class SlackService
- class PipelineMessage < BaseMessage
- attr_reader :sha, :ref_type, :ref, :status, :project_name, :project_url,
- :user_name, :duration, :pipeline_id
-
- def initialize(data)
- pipeline_attributes = data[:object_attributes]
- @sha = pipeline_attributes[:sha]
- @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
- @ref = pipeline_attributes[:ref]
- @status = pipeline_attributes[:status]
- @duration = pipeline_attributes[:duration]
- @pipeline_id = pipeline_attributes[:id]
-
- @project_name = data[:project][:path_with_namespace]
- @project_url = data[:project][:web_url]
- @user_name = data[:commit] && data[:commit][:author_name]
- end
-
- def pretext
- ''
- end
-
- def fallback
- format(message)
- end
-
- def attachments
- [{ text: format(message), color: attachment_color }]
- end
-
- private
-
- def message
- "#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}"
- end
-
- def format(string)
- Slack::Notifier::LinkFormatter.format(string)
- end
-
- def humanized_status
- case status
- when 'success'
- 'passed'
- else
- status
- end
- end
-
- def attachment_color
- if status == 'success'
- 'good'
- else
- 'danger'
- end
- end
-
- def branch_url
- "#{project_url}/commits/#{ref}"
- end
-
- def branch_link
- "[#{ref}](#{branch_url})"
- end
-
- def project_link
- "[#{project_name}](#{project_url})"
- end
-
- def pipeline_url
- "#{project_url}/pipelines/#{pipeline_id}"
- end
-
- def pipeline_link
- "[#{Commit.truncate_sha(sha)}](#{pipeline_url})"
- end
- end
-end
diff --git a/app/models/project_services/slack_service/push_message.rb b/app/models/project_services/slack_service/push_message.rb
deleted file mode 100644
index b26f3e9ddce..00000000000
--- a/app/models/project_services/slack_service/push_message.rb
+++ /dev/null
@@ -1,110 +0,0 @@
-class SlackService
- class PushMessage < BaseMessage
- attr_reader :after
- attr_reader :before
- attr_reader :commits
- attr_reader :project_name
- attr_reader :project_url
- attr_reader :ref
- attr_reader :ref_type
- attr_reader :user_name
-
- def initialize(params)
- @after = params[:after]
- @before = params[:before]
- @commits = params.fetch(:commits, [])
- @project_name = params[:project_name]
- @project_url = params[:project_url]
- @ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch'
- @ref = Gitlab::Git.ref_name(params[:ref])
- @user_name = params[:user_name]
- end
-
- def pretext
- format(message)
- end
-
- def attachments
- return [] if new_branch? || removed_branch?
-
- commit_message_attachments
- end
-
- private
-
- def message
- if new_branch?
- new_branch_message
- elsif removed_branch?
- removed_branch_message
- else
- push_message
- end
- end
-
- def format(string)
- Slack::Notifier::LinkFormatter.format(string)
- end
-
- def new_branch_message
- "#{user_name} pushed new #{ref_type} #{branch_link} to #{project_link}"
- end
-
- def removed_branch_message
- "#{user_name} removed #{ref_type} #{ref} from #{project_link}"
- end
-
- def push_message
- "#{user_name} pushed to #{ref_type} #{branch_link} of #{project_link} (#{compare_link})"
- end
-
- def commit_messages
- commits.map { |commit| compose_commit_message(commit) }.join("\n")
- end
-
- def commit_message_attachments
- [{ text: format(commit_messages), color: attachment_color }]
- end
-
- def compose_commit_message(commit)
- author = commit[:author][:name]
- id = Commit.truncate_sha(commit[:id])
- message = commit[:message]
- url = commit[:url]
-
- "[#{id}](#{url}): #{message} - #{author}"
- end
-
- def new_branch?
- Gitlab::Git.blank_ref?(before)
- end
-
- def removed_branch?
- Gitlab::Git.blank_ref?(after)
- end
-
- def branch_url
- "#{project_url}/commits/#{ref}"
- end
-
- def compare_url
- "#{project_url}/compare/#{before}...#{after}"
- end
-
- def branch_link
- "[#{ref}](#{branch_url})"
- end
-
- def project_link
- "[#{project_name}](#{project_url})"
- end
-
- def compare_link
- "[Compare changes](#{compare_url})"
- end
-
- def attachment_color
- '#345'
- end
- end
-end
diff --git a/app/models/project_services/slack_service/wiki_page_message.rb b/app/models/project_services/slack_service/wiki_page_message.rb
deleted file mode 100644
index 160ca3ac115..00000000000
--- a/app/models/project_services/slack_service/wiki_page_message.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-class SlackService
- class WikiPageMessage < BaseMessage
- attr_reader :user_name
- attr_reader :title
- attr_reader :project_name
- attr_reader :project_url
- attr_reader :wiki_page_url
- attr_reader :action
- attr_reader :description
-
- def initialize(params)
- @user_name = params[:user][:username]
- @project_name = params[:project_name]
- @project_url = params[:project_url]
-
- obj_attr = params[:object_attributes]
- obj_attr = HashWithIndifferentAccess.new(obj_attr)
- @title = obj_attr[:title]
- @wiki_page_url = obj_attr[:url]
- @description = obj_attr[:content]
-
- @action =
- case obj_attr[:action]
- when "create"
- "created"
- when "update"
- "edited"
- end
- end
-
- def attachments
- description_message
- end
-
- private
-
- def message
- "#{user_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*"
- end
-
- def description_message
- [{ text: format(@description), color: attachment_color }]
- end
-
- def project_link
- "[#{project_name}](#{project_url})"
- end
-
- def wiki_page_link
- "[wiki page](#{wiki_page_url})"
- end
- end
-end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index a6e911df9bd..8a53e974b6f 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -21,6 +21,22 @@ class ProjectTeam
end
end
+ def add_guest(user, current_user: nil)
+ self << [user, :guest, current_user]
+ end
+
+ def add_reporter(user, current_user: nil)
+ self << [user, :reporter, current_user]
+ end
+
+ def add_developer(user, current_user: nil)
+ self << [user, :developer, current_user]
+ end
+
+ def add_master(user, current_user: nil)
+ self << [user, :master, current_user]
+ end
+
def find_member(user_id)
member = project.members.find_by(user_id: user_id)
@@ -64,19 +80,19 @@ class ProjectTeam
alias_method :users, :members
def guests
- @guests ||= fetch_members(:guests)
+ @guests ||= fetch_members(Gitlab::Access::GUEST)
end
def reporters
- @reporters ||= fetch_members(:reporters)
+ @reporters ||= fetch_members(Gitlab::Access::REPORTER)
end
def developers
- @developers ||= fetch_members(:developers)
+ @developers ||= fetch_members(Gitlab::Access::DEVELOPER)
end
def masters
- @masters ||= fetch_members(:masters)
+ @masters ||= fetch_members(Gitlab::Access::MASTER)
end
def import(source_project, current_user = nil)
@@ -125,8 +141,12 @@ class ProjectTeam
max_member_access(user.id) == Gitlab::Access::MASTER
end
- def member?(user, min_member_access = Gitlab::Access::GUEST)
- max_member_access(user.id) >= min_member_access
+ # Checks if `user` is authorized for this project, with at least the
+ # `min_access_level` (if given).
+ def member?(user, min_access_level = Gitlab::Access::GUEST)
+ return false unless user
+
+ user.authorized_project?(project, min_access_level)
end
def human_max_access(user_id)
@@ -149,112 +169,29 @@ class ProjectTeam
# Lookup only the IDs we need
user_ids = user_ids - access.keys
+ users_access = project.project_authorizations.
+ where(user: user_ids).
+ group(:user_id).
+ maximum(:access_level)
- if user_ids.present?
- user_ids.each { |id| access[id] = Gitlab::Access::NO_ACCESS }
-
- member_access = project.members.access_for_user_ids(user_ids)
- merge_max!(access, member_access)
-
- if group
- group_access = group.members.access_for_user_ids(user_ids)
- merge_max!(access, group_access)
- end
-
- # Each group produces a list of maximum access level per user. We take the
- # max of the values produced by each group.
- if project_shared_with_group?
- project.project_group_links.each do |group_link|
- invited_access = max_invited_level_for_users(group_link, user_ids)
- merge_max!(access, invited_access)
- end
- end
- end
-
+ access.merge!(users_access)
access
end
def max_member_access(user_id)
- max_member_access_for_user_ids([user_id])[user_id]
+ max_member_access_for_user_ids([user_id])[user_id] || Gitlab::Access::NO_ACCESS
end
private
- # For a given group, return the maximum access level for the user. This is the min of
- # the invited access level of the group and the access level of the user within the group.
- # For example, if the group has been given DEVELOPER access but the member has MASTER access,
- # the user should receive only DEVELOPER access.
- def max_invited_level_for_users(group_link, user_ids)
- invited_group = group_link.group
- capped_access_level = group_link.group_access
- access = invited_group.group_members.access_for_user_ids(user_ids)
-
- # If the user is not in the list, assume he/she does not have access
- missing_users = user_ids - access.keys
- missing_users.each { |id| access[id] = Gitlab::Access::NO_ACCESS }
-
- # Cap the maximum access by the invited level access
- access.each { |key, value| access[key] = [value, capped_access_level].min }
- end
-
def fetch_members(level = nil)
- project_members = project.members
- group_members = group ? group.members : []
-
- if level
- project_members = project_members.public_send(level)
- group_members = group_members.public_send(level) if group
- end
-
- user_ids = project_members.pluck(:user_id)
-
- invited_members = fetch_invited_members(level)
- user_ids.push(*invited_members.map(&:user_id)) if invited_members.any?
+ members = project.authorized_users
+ members = members.where(project_authorizations: { access_level: level }) if level
- user_ids.push(*group_members.pluck(:user_id)) if group
-
- User.where(id: user_ids)
+ members
end
def group
project.group
end
-
- def merge_max!(first_hash, second_hash)
- first_hash.merge!(second_hash) { |_key, old, new| old > new ? old : new }
- end
-
- def project_shared_with_group?
- project.invited_groups.any? && project.allowed_to_share_with_group?
- end
-
- def fetch_invited_members(level = nil)
- invited_members = []
-
- return invited_members unless project_shared_with_group?
-
- project.project_group_links.includes(group: [:group_members]).each do |link|
- invited_group_members = link.group.members
-
- if level
- numeric_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
-
- # If we're asked for a level that's higher than the group's access,
- # there's nothing left to do
- next if numeric_level > link.group_access
-
- # Make sure we include everyone _above_ the requested level as well
- invited_group_members =
- if numeric_level == link.group_access
- invited_group_members.where("access_level >= ?", link.group_access)
- else
- invited_group_members.public_send(level)
- end
- end
-
- invited_members << invited_group_members
- end
-
- invited_members.flatten.compact
- end
end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 46f70da2452..9db96347322 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -127,7 +127,7 @@ class ProjectWiki
end
def search_files(query)
- repository.search_files(query, default_branch)
+ repository.search_files_by_content(query, default_branch)
end
def repository
diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb
index 806b3ccd275..771e3376613 100644
--- a/app/models/protected_branch/merge_access_level.rb
+++ b/app/models/protected_branch/merge_access_level.rb
@@ -1,9 +1,6 @@
class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base
include ProtectedBranchAccess
- belongs_to :protected_branch
- delegate :project, to: :protected_branch
-
validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER,
Gitlab::Access::DEVELOPER] }
@@ -13,10 +10,4 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base
Gitlab::Access::DEVELOPER => "Developers + Masters"
}.with_indifferent_access
end
-
- def check_access(user)
- return true if user.is_admin?
-
- project.team.max_member_access(user.id) >= access_level
- end
end
diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb
index 92e9c51d883..14610cb42b7 100644
--- a/app/models/protected_branch/push_access_level.rb
+++ b/app/models/protected_branch/push_access_level.rb
@@ -1,9 +1,6 @@
class ProtectedBranch::PushAccessLevel < ActiveRecord::Base
include ProtectedBranchAccess
- belongs_to :protected_branch
- delegate :project, to: :protected_branch
-
validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER,
Gitlab::Access::DEVELOPER,
Gitlab::Access::NO_ACCESS] }
@@ -18,8 +15,7 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base
def check_access(user)
return false if access_level == Gitlab::Access::NO_ACCESS
- return true if user.is_admin?
- project.team.max_member_access(user.id) >= access_level
+ super
end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 30be7262438..1ccabdb7c1f 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1,28 +1,56 @@
require 'securerandom'
class Repository
- class CommitError < StandardError; end
-
- # Files to use as a project avatar in case no avatar was uploaded via the web
- # UI.
- AVATAR_FILES = %w{logo.png logo.jpg logo.gif}
-
include Gitlab::ShellAdapter
attr_accessor :path_with_namespace, :project
- def self.storages
- Gitlab.config.repositories.storages
- end
+ class CommitError < StandardError; end
- def self.remove_storage_from_path(repo_path)
- storages.find do |_, storage_path|
- if repo_path.start_with?(storage_path)
- return repo_path.sub(storage_path, '')
- end
+ # Methods that cache data from the Git repository.
+ #
+ # Each entry in this Array should have a corresponding method with the exact
+ # same name. The cache key used by those methods must also match method's
+ # name.
+ #
+ # For example, for entry `:readme` there's a method called `readme` which
+ # stores its data in the `readme` cache key.
+ CACHED_METHODS = %i(size commit_count readme version contribution_guide
+ changelog license_blob license_key gitignore koding_yml
+ gitlab_ci_yml branch_names tag_names branch_count
+ tag_count avatar exists? empty? root_ref)
+
+ # Certain method caches should be refreshed when certain types of files are
+ # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
+ # the corresponding methods to call for refreshing caches.
+ METHOD_CACHES_FOR_FILE_TYPES = {
+ readme: :readme,
+ changelog: :changelog,
+ license: %i(license_blob license_key),
+ contributing: :contribution_guide,
+ version: :version,
+ gitignore: :gitignore,
+ koding: :koding_yml,
+ gitlab_ci: :gitlab_ci_yml,
+ avatar: :avatar
+ }
+
+ # Wraps around the given method and caches its output in Redis and an instance
+ # variable.
+ #
+ # This only works for methods that do not take any arguments.
+ def self.cache_method(name, fallback: nil)
+ original = :"_uncached_#{name}"
+
+ alias_method(original, name)
+
+ define_method(name) do
+ cache_method_output(name, fallback: fallback) { __send__(original) }
end
+ end
- repo_path
+ def self.storages
+ Gitlab.config.repositories.storages
end
def initialize(path_with_namespace, project)
@@ -47,24 +75,6 @@ class Repository
)
end
- def exists?
- return @exists unless @exists.nil?
-
- @exists = cache.fetch(:exists?) do
- begin
- raw_repository && raw_repository.rugged ? true : false
- rescue Gitlab::Git::Repository::NoRepository
- false
- end
- end
- end
-
- def empty?
- return @empty unless @empty.nil?
-
- @empty = cache.fetch(:empty?) { raw_repository.empty? }
- end
-
#
# Git repository can contains some hidden refs like:
# /refs/notes/*
@@ -75,24 +85,22 @@ class Repository
# This method return true if repository contains some content visible in project page.
#
def has_visible_content?
- return @has_visible_content unless @has_visible_content.nil?
-
- @has_visible_content = cache.fetch(:has_visible_content?) do
- branch_count > 0
- end
+ branch_count > 0
end
def commit(ref = 'HEAD')
return nil unless exists?
+
commit =
if ref.is_a?(Gitlab::Git::Commit)
ref
else
Gitlab::Git::Commit.find(raw_repository, ref)
end
+
commit = ::Commit.new(commit, @project) if commit
commit
- rescue Rugged::OdbError
+ rescue Rugged::OdbError, Rugged::TreeError
nil
end
@@ -184,8 +192,9 @@ class Repository
options = { message: message, tagger: user_to_committer(user) } if message
- GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do
- rugged.tags.create(tag_name, target, options)
+ 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
find_tag(tag_name)
@@ -222,16 +231,14 @@ class Repository
branch_names + tag_names
end
- def branch_names
- @branch_names ||= cache.fetch(:branch_names) { branches.map(&:name) }
- end
-
def branch_exists?(branch_name)
branch_names.include?(branch_name)
end
def ref_exists?(ref)
rugged.references.exist?(ref)
+ rescue Rugged::ReferenceError
+ false
end
def update_ref!(name, newrev, oldrev)
@@ -239,7 +246,7 @@ class Repository
# offer 'compare and swap' ref updates. Without compare-and-swap we can
# (and have!) accidentally reset the ref to an earlier state, clobbering
# commits. See also https://github.com/libgit2/libgit2/issues/1534.
- command = %w[git update-ref --stdin -z]
+ command = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z)
_, status = Gitlab::Popen.popen(command, path_to_repo) do |stdin|
stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00")
end
@@ -270,39 +277,7 @@ class Repository
end
def kept_around?(sha)
- begin
- ref_exists?(keep_around_ref_name(sha))
- rescue Rugged::ReferenceError
- false
- end
- end
-
- def tag_names
- cache.fetch(:tag_names) { raw_repository.tag_names }
- end
-
- def commit_count
- cache.fetch(:commit_count) do
- begin
- raw_repository.commit_count(self.root_ref)
- rescue
- 0
- end
- end
- end
-
- def branch_count
- @branch_count ||= cache.fetch(:branch_count) { branches.size }
- end
-
- def tag_count
- @tag_count ||= cache.fetch(:tag_count) { raw_repository.rugged.tags.count }
- end
-
- # Return repo size in megabytes
- # Cached in redis
- def size
- cache.fetch(:size) { raw_repository.size }
+ ref_exists?(keep_around_ref_name(sha))
end
def diverging_commit_counts(branch)
@@ -320,48 +295,55 @@ class Repository
end
end
- # Keys for data that can be affected for any commit push.
- def cache_keys
- %i(size commit_count
- readme version contribution_guide changelog
- license_blob license_key gitignore koding_yml)
+ def expire_tags_cache
+ expire_method_caches(%i(tag_names tag_count))
+ @tags = nil
end
- # Keys for data on branch/tag operations.
- def cache_keys_for_branches_and_tags
- %i(branch_names tag_names branch_count tag_count)
+ def expire_branches_cache
+ expire_method_caches(%i(branch_names branch_count))
+ @local_branches = nil
end
- def build_cache
- (cache_keys + cache_keys_for_branches_and_tags).each do |key|
- unless cache.exist?(key)
- send(key)
- end
- end
+ def expire_statistics_caches
+ expire_method_caches(%i(size commit_count))
end
- def expire_tags_cache
- cache.expire(:tag_names)
- @tags = nil
+ def expire_all_method_caches
+ expire_method_caches(CACHED_METHODS)
end
- def expire_branches_cache
- cache.expire(:branch_names)
- @branch_names = nil
- @local_branches = nil
+ # Expires the caches of a specific set of methods
+ def expire_method_caches(methods)
+ methods.each do |key|
+ cache.expire(key)
+
+ ivar = cache_instance_variable_name(key)
+
+ remove_instance_variable(ivar) if instance_variable_defined?(ivar)
+ end
end
- def expire_cache(branch_name = nil, revision = nil)
- cache_keys.each do |key|
- cache.expire(key)
+ def expire_avatar_cache
+ expire_method_caches(%i(avatar))
+ end
+
+ # Refreshes the method caches of this repository.
+ #
+ # types - An Array of file types (e.g. `:readme`) used to refresh extra
+ # caches.
+ def refresh_method_caches(types)
+ to_refresh = []
+
+ types.each do |type|
+ methods = METHOD_CACHES_FOR_FILE_TYPES[type.to_sym]
+
+ to_refresh.concat(Array(methods)) if methods
end
- expire_branch_cache(branch_name)
- expire_avatar_cache(branch_name, revision)
+ expire_method_caches(to_refresh)
- # This ensures this particular cache is flushed after the first commit to a
- # new repository.
- expire_emptiness_caches if empty?
+ to_refresh.each { |method| send(method) }
end
def expire_branch_cache(branch_name = nil)
@@ -380,68 +362,32 @@ class Repository
end
def expire_root_ref_cache
- cache.expire(:root_ref)
- @root_ref = nil
+ expire_method_caches(%i(root_ref))
end
# Expires the cache(s) used to determine if a repository is empty or not.
def expire_emptiness_caches
- cache.expire(:empty?)
- @empty = nil
-
- expire_has_visible_content_cache
- end
-
- def expire_has_visible_content_cache
- cache.expire(:has_visible_content?)
- @has_visible_content = nil
- end
+ return unless empty?
- def expire_branch_count_cache
- cache.expire(:branch_count)
- @branch_count = nil
- end
-
- def expire_tag_count_cache
- cache.expire(:tag_count)
- @tag_count = nil
+ expire_method_caches(%i(empty?))
end
def lookup_cache
@lookup_cache ||= {}
end
- def expire_avatar_cache(branch_name = nil, revision = nil)
- # Avatars are pulled from the default branch, thus if somebody pushes to a
- # different branch there's no need to expire anything.
- return if branch_name && branch_name != root_ref
-
- # We don't want to flush the cache if the commit didn't actually make any
- # changes to any of the possible avatar files.
- if revision && commit = self.commit(revision)
- return unless commit.raw_diffs(deltas_only: true).
- any? { |diff| AVATAR_FILES.include?(diff.new_path) }
- end
-
- cache.expire(:avatar)
-
- @avatar = nil
- end
-
def expire_exists_cache
- cache.expire(:exists?)
- @exists = nil
+ expire_method_caches(%i(exists?))
end
# expire cache that doesn't depend on repository data (when expiring)
def expire_content_cache
expire_tags_cache
- expire_tag_count_cache
expire_branches_cache
- expire_branch_count_cache
expire_root_ref_cache
expire_emptiness_caches
expire_exists_cache
+ expire_statistics_caches
end
# Runs code after a repository has been created.
@@ -456,9 +402,8 @@ class Repository
# Runs code just before a repository is deleted.
def before_delete
expire_exists_cache
-
- expire_cache if exists?
-
+ expire_all_method_caches
+ expire_branch_cache if exists?
expire_content_cache
repository_event(:remove_repository)
@@ -475,9 +420,9 @@ class Repository
# Runs code before pushing (= creating or removing) a tag.
def before_push_tag
- expire_cache
+ expire_statistics_caches
+ expire_emptiness_caches
expire_tags_cache
- expire_tag_count_cache
repository_event(:push_tag)
end
@@ -485,7 +430,7 @@ class Repository
# Runs code before removing a tag.
def before_remove_tag
expire_tags_cache
- expire_tag_count_cache
+ expire_statistics_caches
repository_event(:remove_tag)
end
@@ -497,12 +442,14 @@ class Repository
# Runs code after a repository has been forked/imported.
def after_import
expire_content_cache
- build_cache
+ expire_tags_cache
+ expire_branches_cache
end
# Runs code after a new commit has been pushed.
- def after_push_commit(branch_name, revision)
- expire_cache(branch_name, revision)
+ def after_push_commit(branch_name)
+ expire_statistics_caches
+ expire_branch_cache(branch_name)
repository_event(:push_commit, branch: branch_name)
end
@@ -510,8 +457,6 @@ class Repository
# Runs code after a new branch has been created.
def after_create_branch
expire_branches_cache
- expire_has_visible_content_cache
- expire_branch_count_cache
repository_event(:push_branch)
end
@@ -525,8 +470,6 @@ class Repository
# Runs code after an existing branch has been removed.
def after_remove_branch
- expire_has_visible_content_cache
- expire_branch_count_cache
expire_branches_cache
end
@@ -553,86 +496,127 @@ class Repository
Gitlab::Git::Blob.raw(self, oid)
end
+ def root_ref
+ if raw_repository
+ raw_repository.root_ref
+ else
+ # When the repo does not exist we raise this error so no data is cached.
+ raise Rugged::ReferenceError
+ end
+ end
+ cache_method :root_ref
+
+ def exists?
+ refs_directory_exists?
+ end
+ cache_method :exists?
+
+ def empty?
+ raw_repository.empty?
+ end
+ cache_method :empty?
+
+ # The size of this repository in megabytes.
+ def size
+ exists? ? raw_repository.size : 0.0
+ end
+ cache_method :size, fallback: 0.0
+
+ def commit_count
+ root_ref ? raw_repository.commit_count(root_ref) : 0
+ end
+ cache_method :commit_count, fallback: 0
+
+ def branch_names
+ branches.map(&:name)
+ end
+ cache_method :branch_names, fallback: []
+
+ def tag_names
+ raw_repository.tag_names
+ end
+ cache_method :tag_names, fallback: []
+
+ def branch_count
+ branches.size
+ end
+ cache_method :branch_count, fallback: 0
+
+ def tag_count
+ raw_repository.rugged.tags.count
+ end
+ cache_method :tag_count, fallback: 0
+
+ def avatar
+ if tree = file_on_head(:avatar)
+ tree.path
+ end
+ end
+ cache_method :avatar
+
def readme
- cache.fetch(:readme) { tree(:head).readme }
+ if head = tree(:head)
+ head.readme
+ end
end
+ cache_method :readme
def version
- cache.fetch(:version) do
- tree(:head).blobs.find do |file|
- file.name.casecmp('version').zero?
- end
- end
+ file_on_head(:version)
end
+ cache_method :version
def contribution_guide
- cache.fetch(:contribution_guide) do
- tree(:head).blobs.find do |file|
- file.contributing?
- end
- end
+ file_on_head(:contributing)
end
+ cache_method :contribution_guide
def changelog
- cache.fetch(:changelog) do
- file_on_head(/\A(changelog|history|changes|news)/i)
- end
+ file_on_head(:changelog)
end
+ cache_method :changelog
def license_blob
- return nil unless head_exists?
-
- cache.fetch(:license_blob) do
- file_on_head(/\A(licen[sc]e|copying)(\..+|\z)/i)
- end
+ file_on_head(:license)
end
+ cache_method :license_blob
def license_key
- return nil unless head_exists?
+ return unless exists?
- cache.fetch(:license_key) do
- Licensee.license(path).try(:key)
- end
+ Licensee.license(path).try(:key)
end
+ cache_method :license_key
def gitignore
- return nil if !exists? || empty?
-
- cache.fetch(:gitignore) do
- file_on_head(/\A\.gitignore\z/)
- end
+ file_on_head(:gitignore)
end
+ cache_method :gitignore
def koding_yml
- return nil unless head_exists?
-
- cache.fetch(:koding_yml) do
- file_on_head(/\A\.koding\.yml\z/)
- end
+ file_on_head(:koding)
end
+ cache_method :koding_yml
def gitlab_ci_yml
- return nil unless head_exists?
-
- @gitlab_ci_yml ||= tree(:head).blobs.find do |file|
- file.name == '.gitlab-ci.yml'
- end
- rescue Rugged::ReferenceError
- # For unknow reason spinach scenario "Scenario: I change project path"
- # lead to "Reference 'HEAD' not found" exception from Repository#empty?
- nil
+ file_on_head(:gitlab_ci)
end
+ cache_method :gitlab_ci_yml
def head_commit
@head_commit ||= commit(self.root_ref)
end
def head_tree
- @head_tree ||= Tree.new(self, head_commit.sha, nil)
+ if head_commit
+ @head_tree ||= Tree.new(self, head_commit.sha, nil)
+ end
end
- def tree(sha = :head, path = nil)
+ def tree(sha = :head, path = nil, recursive: false)
if sha == :head
+ return unless head_commit
+
if path.nil?
return head_tree
else
@@ -640,7 +624,7 @@ class Repository
end
end
- Tree.new(self, sha, path)
+ Tree.new(self, sha, path, recursive: recursive)
end
def blob_at_branch(branch_name, path)
@@ -782,10 +766,6 @@ class Repository
@tags ||= raw_repository.tags
end
- def root_ref
- @root_ref ||= cache.fetch(:root_ref) { raw_repository.root_ref }
- end
-
def commit_dir(user, path, message, branch, author_email: nil, author_name: nil)
update_branch_with_hooks(user, branch) do |ref|
options = {
@@ -970,7 +950,7 @@ class Repository
update_branch_with_hooks(user, base_branch) do
committer = user_to_committer(user)
source_sha = Rugged::Commit.create(rugged,
- message: commit.revert_message,
+ message: commit.revert_message(user),
author: committer,
committer: committer,
tree: revert_tree_id,
@@ -1063,12 +1043,25 @@ class Repository
merge_base(ancestor_id, descendant_id) == ancestor_id
end
- def search_files(query, ref)
+ def empty_repo?
+ !exists? || !has_visible_content?
+ end
+
+ def search_files_by_content(query, ref)
+ return [] if empty_repo? || query.blank?
+
offset = 2
args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
end
+ def search_files_by_name(query, ref)
+ return [] if empty_repo? || query.blank?
+
+ args = %W(#{Gitlab.config.git.bin_path} ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)})
+ Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:strip)
+ end
+
def fetch_ref(source_path, source_ref, target_ref)
args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
Gitlab::Popen.popen(args, path_to_repo)
@@ -1130,28 +1123,55 @@ class Repository
end
end
- def avatar
- return nil unless exists?
+ # Caches the supplied block both in a cache and in an instance variable.
+ #
+ # The cache key and instance variable are named the same way as the value of
+ # the `key` argument.
+ #
+ # This method will return `nil` if the corresponding instance variable is also
+ # set to `nil`. This ensures we don't keep yielding the block when it returns
+ # `nil`.
+ #
+ # key - The name of the key to cache the data in.
+ # fallback - A value to fall back to in the event of a Git error.
+ def cache_method_output(key, fallback: nil, &block)
+ ivar = cache_instance_variable_name(key)
+
+ if instance_variable_defined?(ivar)
+ instance_variable_get(ivar)
+ else
+ begin
+ instance_variable_set(ivar, cache.fetch(key, &block))
+ rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository
+ # if e.g. HEAD or the entire repository doesn't exist we want to
+ # gracefully handle this and not cache anything.
+ fallback
+ end
+ end
+ end
+
+ def cache_instance_variable_name(key)
+ :"@#{key.to_s.tr('?!', '')}"
+ end
- @avatar ||= cache.fetch(:avatar) do
- AVATAR_FILES.find do |file|
- blob_at_branch(root_ref, file)
+ def file_on_head(type)
+ if head = tree(:head)
+ head.blobs.find do |file|
+ Gitlab::FileDetector.type_of(file.name) == type
end
end
end
private
- def cache
- @cache ||= RepositoryCache.new(path_with_namespace, @project.id)
- end
+ def refs_directory_exists?
+ return false unless path_with_namespace
- def head_exists?
- exists? && !empty? && !rugged.head_unborn?
+ File.exist?(File.join(path_to_repo, 'refs'))
end
- def file_on_head(regex)
- tree(:head).blobs.find { |file| file.name =~ regex }
+ def cache
+ @cache ||= RepositoryCache.new(path_with_namespace, @project.id)
end
def tags_sorted_by_committed_date
diff --git a/app/models/route.rb b/app/models/route.rb
new file mode 100644
index 00000000000..d40214b9da6
--- /dev/null
+++ b/app/models/route.rb
@@ -0,0 +1,22 @@
+class Route < ActiveRecord::Base
+ belongs_to :source, polymorphic: true
+
+ validates :source, presence: true
+
+ validates :path,
+ length: { within: 1..255 },
+ presence: true,
+ uniqueness: { case_sensitive: false }
+
+ after_update :rename_children, if: :path_changed?
+
+ def rename_children
+ # 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_children method
+ route.update_column(:path, route.path.sub(path_was, path))
+ end
+ end
+end
diff --git a/app/models/service.rb b/app/models/service.rb
index 625fbc48302..0bbab078cf6 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -8,6 +8,7 @@ class Service < ActiveRecord::Base
default_value_for :push_events, true
default_value_for :issues_events, true
default_value_for :confidential_issues_events, true
+ default_value_for :commit_events, true
default_value_for :merge_requests_events, true
default_value_for :tag_push_events, true
default_value_for :note_events, true
@@ -202,7 +203,6 @@ class Service < ActiveRecord::Base
bamboo
buildkite
builds_email
- pipelines_email
bugzilla
campfire
custom_issue_tracker
@@ -214,19 +214,23 @@ class Service < ActiveRecord::Base
hipchat
irker
jira
+ kubernetes
+ mattermost_slash_commands
+ pipelines_email
pivotaltracker
pushover
redmine
- slack
+ mattermost_notification
+ slack_notification
teamcity
]
end
- def self.create_from_template(project_id, template)
+ def self.build_from_template(project_id, template)
service = template.dup
service.template = false
service.project_id = project_id
- service if service.save
+ service
end
private
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 2373b445009..98ccf5f331f 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -6,6 +6,7 @@ class Snippet < ActiveRecord::Base
include Referable
include Sortable
include Awardable
+ include Mentionable
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :content
@@ -26,9 +27,9 @@ class Snippet < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true, allow_nil: true
validates :author, presence: true
- validates :title, presence: true, length: { within: 0..255 }
+ validates :title, presence: true, length: { maximum: 255 }
validates :file_name,
- length: { within: 0..255 },
+ length: { maximum: 255 },
format: { with: Gitlab::Regex.file_name_regex,
message: Gitlab::Regex.file_name_regex_message }
@@ -66,11 +67,11 @@ class Snippet < ActiveRecord::Base
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{id}"
- if cross_project_reference?(from_project)
- reference = project.to_reference + reference
+ if project.present?
+ "#{project.to_reference(from_project)}#{reference}"
+ else
+ reference
end
-
- reference
end
def self.content_types
@@ -93,6 +94,10 @@ class Snippet < ActiveRecord::Base
0
end
+ def file_name
+ super.to_s
+ end
+
# alias for compatibility with blobs and highlighting
def path
file_name
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
index 3b8aa1eb866..17869c8bac2 100644
--- a/app/models/subscription.rb
+++ b/app/models/subscription.rb
@@ -1,8 +1,9 @@
class Subscription < ActiveRecord::Base
belongs_to :user
+ belongs_to :project
belongs_to :subscribable, polymorphic: true
- validates :user_id,
- uniqueness: { scope: [:subscribable_id, :subscribable_type] },
- presence: true
+ validates :user, :subscribable, presence: true
+
+ validates :project_id, uniqueness: { scope: [:subscribable_id, :subscribable_type, :user_id] }
end
diff --git a/app/models/tree.rb b/app/models/tree.rb
index 7c4ed6e393b..fe148b0ec65 100644
--- a/app/models/tree.rb
+++ b/app/models/tree.rb
@@ -3,21 +3,24 @@ class Tree
attr_accessor :repository, :sha, :path, :entries
- def initialize(repository, sha, path = '/')
+ def initialize(repository, sha, path = '/', recursive: false)
path = '/' if path.blank?
@repository = repository
@sha = sha
@path = path
+ @recursive = recursive
git_repo = @repository.raw_repository
- @entries = Gitlab::Git::Tree.where(git_repo, @sha, @path)
+ @entries = get_entries(git_repo, @sha, @path, recursive: @recursive)
end
def readme
return @readme if defined?(@readme)
- available_readmes = blobs.select(&:readme?)
+ available_readmes = blobs.select do |blob|
+ Gitlab::FileDetector.type_of(blob.name) == :readme
+ end
previewable_readmes = available_readmes.select do |blob|
previewable?(blob.name)
@@ -58,4 +61,21 @@ class Tree
def sorted_entries
trees + blobs + submodules
end
+
+ private
+
+ def get_entries(git_repo, sha, path, recursive: false)
+ current_path_entries = Gitlab::Git::Tree.where(git_repo, sha, path)
+ ordered_entries = []
+
+ current_path_entries.each do |entry|
+ ordered_entries << entry
+
+ if recursive && entry.dir?
+ ordered_entries.concat(get_entries(git_repo, sha, entry.path, recursive: true))
+ end
+ end
+
+ ordered_entries
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 3813df6684e..3a17c98eff6 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -56,6 +56,7 @@ class User < ActiveRecord::Base
has_many :personal_access_tokens, dependent: :destroy
has_many :identities, dependent: :destroy, autosave: true
has_many :u2f_registrations, dependent: :destroy
+ has_many :chat_names, dependent: :destroy
# Groups
has_many :members, dependent: :destroy
@@ -72,6 +73,8 @@ class User < ActiveRecord::Base
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy
has_many :starred_projects, through: :users_star_projects, source: :project
+ has_many :project_authorizations, dependent: :destroy
+ has_many :authorized_projects, through: :project_authorizations, source: :project
has_many :snippets, dependent: :destroy, foreign_key: :author_id
has_many :issues, dependent: :destroy, foreign_key: :author_id
@@ -173,8 +176,10 @@ class User < ActiveRecord::Base
scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
- scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
+ scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
+ scope :order_recent_sign_in, -> { reorder(last_sign_in_at: :desc) }
+ scope :order_oldest_sign_in, -> { reorder(last_sign_in_at: :asc) }
def self.with_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
@@ -202,8 +207,8 @@ class User < ActiveRecord::Base
def sort(method)
case method.to_s
- when 'recent_sign_in' then reorder(last_sign_in_at: :desc)
- when 'oldest_sign_in' then reorder(last_sign_in_at: :asc)
+ when 'recent_sign_in' then order_recent_sign_in
+ when 'oldest_sign_in' then order_oldest_sign_in
else
order_by(method)
end
@@ -226,19 +231,19 @@ class User < ActiveRecord::Base
def filter(filter_name)
case filter_name
when 'admins'
- self.admins
+ admins
when 'blocked'
- self.blocked
+ blocked
when 'two_factor_disabled'
- self.without_two_factor
+ without_two_factor
when 'two_factor_enabled'
- self.with_two_factor
+ with_two_factor
when 'wop'
- self.without_projects
+ without_projects
when 'external'
- self.external
+ external
else
- self.active
+ active
end
end
@@ -288,8 +293,12 @@ class User < ActiveRecord::Base
end
end
+ def find_by_username(username)
+ iwhere(username: username).take
+ end
+
def find_by_username!(username)
- find_by!('lower(username) = ?', username.downcase)
+ iwhere(username: username).take!
end
def find_by_personal_access_token(token_string)
@@ -297,10 +306,6 @@ class User < ActiveRecord::Base
personal_access_token.user if personal_access_token
end
- def by_username_or_id(name_or_id)
- find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
- end
-
# Returns a user for the given SSH key.
def find_by_ssh_key_id(key_id)
find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
@@ -336,7 +341,7 @@ class User < ActiveRecord::Base
end
def generate_password
- if self.force_random_password
+ if force_random_password
self.password = self.password_confirmation = Devise.friendly_token.first(Devise.password_length.min)
end
end
@@ -377,56 +382,55 @@ class User < ActiveRecord::Base
end
def two_factor_otp_enabled?
- self.otp_required_for_login?
+ otp_required_for_login?
end
def two_factor_u2f_enabled?
- self.u2f_registrations.exists?
+ u2f_registrations.exists?
end
def namespace_uniq
# Return early if username already failed the first uniqueness validation
- return if self.errors.key?(:username) &&
- self.errors[:username].include?('has already been taken')
+ return if errors.key?(:username) &&
+ errors[:username].include?('has already been taken')
- namespace_name = self.username
- existing_namespace = Namespace.by_path(namespace_name)
- if existing_namespace && existing_namespace != self.namespace
- self.errors.add(:username, 'has already been taken')
+ existing_namespace = Namespace.by_path(username)
+ if existing_namespace && existing_namespace != namespace
+ errors.add(:username, 'has already been taken')
end
end
def avatar_type
- unless self.avatar.image?
- self.errors.add :avatar, "only images allowed"
+ unless avatar.image?
+ errors.add :avatar, "only images allowed"
end
end
def unique_email
- if !self.emails.exists?(email: self.email) && Email.exists?(email: self.email)
- self.errors.add(:email, 'has already been taken')
+ if !emails.exists?(email: email) && Email.exists?(email: email)
+ errors.add(:email, 'has already been taken')
end
end
def owns_notification_email
- return if self.temp_oauth_email?
+ return if temp_oauth_email?
- self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email)
+ errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email)
end
def owns_public_email
- return if self.public_email.blank?
+ return if public_email.blank?
- self.errors.add(:public_email, "is not an email you own") unless self.all_emails.include?(self.public_email)
+ errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
end
def update_emails_with_primary_email
- primary_email_record = self.emails.find_by(email: self.email)
+ primary_email_record = emails.find_by(email: email)
if primary_email_record
primary_email_record.destroy
- self.emails.create(email: self.email_was)
+ emails.create(email: email_was)
- self.update_secondary_emails!
+ update_secondary_emails!
end
end
@@ -438,11 +442,38 @@ class User < ActiveRecord::Base
Group.where("namespaces.id IN (#{union.to_sql})")
end
- # Returns projects user is authorized to access.
- #
- # If you change the logic of this method, please also update `Project#authorized_for_user`
+ def refresh_authorized_projects
+ transaction do
+ project_authorizations.delete_all
+
+ # project_authorizations_union can return multiple records for the same
+ # project/user with different access_level so we take row with the maximum
+ # access_level
+ project_authorizations.connection.execute <<-SQL
+ INSERT INTO project_authorizations (user_id, project_id, access_level)
+ SELECT user_id, project_id, MAX(access_level) AS access_level
+ FROM (#{project_authorizations_union.to_sql}) sub
+ GROUP BY user_id, project_id
+ SQL
+
+ unless authorized_projects_populated
+ update_column(:authorized_projects_populated, true)
+ end
+ end
+ end
+
def authorized_projects(min_access_level = nil)
- Project.where("projects.id IN (#{projects_union(min_access_level).to_sql})")
+ refresh_authorized_projects unless authorized_projects_populated
+
+ # We're overriding an association, so explicitly call super with no arguments or it would be passed as `force_reload` to the association
+ projects = super()
+ projects = projects.where('project_authorizations.access_level >= ?', min_access_level) if min_access_level
+
+ projects
+ end
+
+ def authorized_project?(project, min_access_level = nil)
+ authorized_projects(min_access_level).exists?({ id: project.id })
end
# Returns the projects this user has reporter (or greater) access to, limited
@@ -456,8 +487,9 @@ class User < ActiveRecord::Base
end
def viewable_starred_projects
- starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (#{projects_union.to_sql})",
- [Project::PUBLIC, Project::INTERNAL])
+ starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (?)",
+ [Project::PUBLIC, Project::INTERNAL],
+ authorized_projects.select(:project_id))
end
def owned_projects
@@ -478,7 +510,7 @@ class User < ActiveRecord::Base
end
def require_ssh_key?
- keys.count == 0
+ keys.count == 0 && Gitlab::ProtocolAccess.allowed?('ssh')
end
def require_password?
@@ -580,7 +612,7 @@ class User < ActiveRecord::Base
end
def project_deploy_keys
- DeployKey.unscoped.in_projects(self.authorized_projects.pluck(:id)).distinct(:id)
+ DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
end
def accessible_deploy_keys
@@ -596,38 +628,38 @@ class User < ActiveRecord::Base
end
def sanitize_attrs
- %w(name username skype linkedin twitter).each do |attr|
- value = self.send(attr)
- self.send("#{attr}=", Sanitize.clean(value)) if value.present?
+ %w[name username skype linkedin twitter].each do |attr|
+ value = public_send(attr)
+ public_send("#{attr}=", Sanitize.clean(value)) if value.present?
end
end
def set_notification_email
- if self.notification_email.blank? || !self.all_emails.include?(self.notification_email)
- self.notification_email = self.email
+ if notification_email.blank? || !all_emails.include?(notification_email)
+ self.notification_email = email
end
end
def set_public_email
- if self.public_email.blank? || !self.all_emails.include?(self.public_email)
+ if public_email.blank? || !all_emails.include?(public_email)
self.public_email = ''
end
end
def update_secondary_emails!
- self.set_notification_email
- self.set_public_email
- self.save if self.notification_email_changed? || self.public_email_changed?
+ set_notification_email
+ set_public_email
+ save if notification_email_changed? || public_email_changed?
end
def set_projects_limit
# `User.select(:id)` raises
# `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
# without this safeguard!
- return unless self.has_attribute?(:projects_limit)
+ return unless has_attribute?(:projects_limit)
connection_default_value_defined = new_record? && !projects_limit_changed?
- return unless self.projects_limit.nil? || connection_default_value_defined
+ return unless projects_limit.nil? || connection_default_value_defined
self.projects_limit = current_application_settings.default_projects_limit
end
@@ -657,7 +689,7 @@ class User < ActiveRecord::Base
def with_defaults
User.defaults.each do |k, v|
- self.send("#{k}=", v)
+ public_send("#{k}=", v)
end
self
@@ -668,20 +700,6 @@ class User < ActiveRecord::Base
project.project_member(self)
end
- # Reset project events cache related to this user
- #
- # Since we do cache @event we need to reset cache in special cases:
- # * when the user changes their avatar
- # Events cache stored like events/23-20130109142513.
- # The cache key includes updated_at timestamp.
- # Thus it will automatically generate a new fragment
- # when the event is updated because the key changes.
- def reset_events_cache
- Event.where(author_id: self.id).
- order('id DESC').limit(1000).
- update_all(updated_at: Time.now)
- end
-
def full_website_url
return "http://#{website_url}" if website_url !~ /\Ahttps?:\/\//
@@ -710,8 +728,8 @@ class User < ActiveRecord::Base
def all_emails
all_emails = []
- all_emails << self.email unless self.temp_oauth_email?
- all_emails.concat(self.emails.map(&:email))
+ all_emails << email unless temp_oauth_email?
+ all_emails.concat(emails.map(&:email))
all_emails
end
@@ -725,21 +743,21 @@ class User < ActiveRecord::Base
def ensure_namespace_correct
# Ensure user has namespace
- self.create_namespace!(path: self.username, name: self.username) unless self.namespace
+ create_namespace!(path: username, name: username) unless namespace
- if self.username_changed?
- self.namespace.update_attributes(path: self.username, name: self.username)
+ if username_changed?
+ namespace.update_attributes(path: username, name: username)
end
end
def post_create_hook
- log_info("User \"#{self.name}\" (#{self.email}) was created")
- notification_service.new_user(self, @reset_token) if self.created_by_id
+ log_info("User \"#{name}\" (#{email}) was created")
+ notification_service.new_user(self, @reset_token) if created_by_id
system_hook_service.execute_hooks_for(self, :create)
end
def post_destroy_hook
- log_info("User \"#{self.name}\" (#{self.email}) was removed")
+ log_info("User \"#{name}\" (#{email}) was removed")
system_hook_service.execute_hooks_for(self, :destroy)
end
@@ -783,7 +801,7 @@ class User < ActiveRecord::Base
end
def oauth_authorized_tokens
- Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil)
+ Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
end
# Returns the projects a user contributed to in the last year.
@@ -887,16 +905,14 @@ class User < ActiveRecord::Base
private
- def projects_union(min_access_level = nil)
- relations = [personal_projects.select(:id),
- groups_projects.select(:id),
- projects.select(:id),
- groups.joins(:shared_projects).select(:project_id)]
-
- if min_access_level
- scope = { access_level: Gitlab::Access.all_values.select { |access| access >= min_access_level } }
- relations = [relations.shift] + relations.map { |relation| relation.where(members: scope) }
- end
+ # Returns a union query of projects that the user is authorized to access
+ def project_authorizations_union
+ relations = [
+ personal_projects.select("#{id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"),
+ groups_projects.select_for_project_authorization,
+ projects.select_for_project_authorization,
+ groups.joins(:shared_projects).select_for_project_authorization
+ ]
Gitlab::SQL::Union.new(relations)
end
@@ -916,7 +932,7 @@ class User < ActiveRecord::Base
end
def ensure_external_user_rights
- return unless self.external?
+ return unless external?
self.can_create_group = false
self.projects_limit = 0
@@ -928,7 +944,7 @@ class User < ActiveRecord::Base
if current_application_settings.domain_blacklist_enabled?
blocked_domains = current_application_settings.domain_blacklist
- if domain_matches?(blocked_domains, self.email)
+ if domain_matches?(blocked_domains, email)
error = 'is not from an allowed domain.'
valid = false
end
@@ -936,7 +952,7 @@ class User < ActiveRecord::Base
allowed_domains = current_application_settings.domain_whitelist
unless allowed_domains.blank?
- if domain_matches?(allowed_domains, self.email)
+ if domain_matches?(allowed_domains, email)
valid = true
else
error = "domain is not authorized for sign-up"
@@ -944,7 +960,7 @@ class User < ActiveRecord::Base
end
end
- self.errors.add(:email, error) unless valid
+ errors.add(:email, error) unless valid
valid
end
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 2232e231cf8..7b1752df0e1 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -1,11 +1,13 @@
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
# ability when looking at this in context of commit_status
- %w(read create update admin).each do |rule|
+ %w[read create update admin].each do |rule|
cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build"
end
end
diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
new file mode 100644
index 00000000000..3d2eef1c50c
--- /dev/null
+++ b/app/policies/ci/pipeline_policy.rb
@@ -0,0 +1,4 @@
+module Ci
+ class PipelinePolicy < BuildPolicy
+ end
+end
diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb
index 62335527654..5a3fe814b77 100644
--- a/app/policies/group_member_policy.rb
+++ b/app/policies/group_member_policy.rb
@@ -15,5 +15,11 @@ class GroupMemberPolicy < BasePolicy
elsif @user == target_user
can! :destroy_group_member
end
+
+ additional_rules!
+ end
+
+ def additional_rules!
+ # This is meant to be overriden in EE
end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index b65fb68cd88..6f943feb2a7 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -33,6 +33,8 @@ class GroupPolicy < BasePolicy
if globally_viewable && @subject.request_access_enabled && !member
can! :request_access
end
+
+ additional_rules!(master)
end
def can_read_group?
@@ -43,4 +45,8 @@ class GroupPolicy < BasePolicy
GroupProjectsFinder.new(@subject).execute(@user).any?
end
+
+ def additional_rules!(master)
+ # This is meant to be overriden in EE
+ end
end
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index 52fa33bc4b0..88f3179c6ff 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -1,4 +1,8 @@
class IssuePolicy < IssuablePolicy
+ # This class duplicates the same check of Issue#readable_by? for performance reasons
+ # Make sure to sync this class checks with issue.rb to avoid security problems.
+ # Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information.
+
def issue
@subject
end
diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb
index 83847466ee2..5326061bd07 100644
--- a/app/policies/note_policy.rb
+++ b/app/policies/note_policy.rb
@@ -12,7 +12,7 @@ class NotePolicy < BasePolicy
end
if @subject.for_merge_request? &&
- @subject.noteable.author == @user
+ @subject.noteable.author == @user
can! :resolve_note
end
end
diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb
index 46c5aa1a5be..d3913986cd8 100644
--- a/app/policies/personal_snippet_policy.rb
+++ b/app/policies/personal_snippet_policy.rb
@@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy
if @subject.author == @user
can! :read_personal_snippet
can! :update_personal_snippet
+ can! :destroy_personal_snippet
can! :admin_personal_snippet
end
+ unless @user.external?
+ can! :create_personal_snippet
+ end
+
if @subject.internal? && !@user.external?
can! :read_personal_snippet
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 1ee31023e26..b5db9c12622 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -3,7 +3,7 @@ class ProjectPolicy < BasePolicy
team_access!(user)
owner = project.owner == user ||
- (project.group && project.group.has_owner?(user))
+ (project.group && project.group.has_owner?(user))
owner_access! if user.admin? || owner
team_member_owner_access! if owner
@@ -12,11 +12,8 @@ class ProjectPolicy < BasePolicy
guest_access!
public_access!
- # Allow to read builds for internal projects
- can! :read_build if project.public_builds?
-
if project.request_access_enabled &&
- !(owner || user.admin? || project.team.member?(user) || project_group_member?(user))
+ !(owner || user.admin? || project.team.member?(user) || project_group_member?(user))
can! :request_access
end
end
@@ -46,10 +43,16 @@ class ProjectPolicy < BasePolicy
can! :create_note
can! :upload_file
can! :read_cycle_analytics
+
+ if project.public_builds?
+ can! :read_pipeline
+ can! :read_build
+ end
end
def reporter_access!
can! :download_code
+ can! :download_wiki_code
can! :fork_project
can! :create_project_snippet
can! :update_issue
@@ -187,6 +190,7 @@ class ProjectPolicy < BasePolicy
unless project.feature_available?(:wiki, user) || project.has_external_wiki?
cannot!(*named_abilities(:wiki))
+ cannot!(:download_wiki_code)
end
unless project.feature_available?(:builds, user) && repository_enabled
@@ -226,6 +230,7 @@ class ProjectPolicy < BasePolicy
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
@@ -239,10 +244,10 @@ class ProjectPolicy < BasePolicy
def project_group_member?(user)
project.group &&
- (
- project.group.members.exists?(user_id: user.id) ||
- project.group.requesters.exists?(user_id: user.id)
- )
+ (
+ project.group.members.exists?(user_id: user.id) ||
+ project.group.requesters.exists?(user_id: user.id)
+ )
end
def named_abilities(name)
diff --git a/app/serializers/analytics_build_entity.rb b/app/serializers/analytics_build_entity.rb
new file mode 100644
index 00000000000..a0db5b8f0f4
--- /dev/null
+++ b/app/serializers/analytics_build_entity.rb
@@ -0,0 +1,40 @@
+class AnalyticsBuildEntity < Grape::Entity
+ include RequestAwareEntity
+ include EntityDateHelper
+
+ expose :name
+ expose :id
+ expose :ref, as: :branch
+ expose :short_sha
+ expose :author, using: UserEntity
+
+ expose :started_at, as: :date do |build|
+ interval_in_words(build[:started_at])
+ end
+
+ expose :duration, as: :total_time do |build|
+ build.duration ? distance_of_time_as_hash(build.duration.to_f) : {}
+ end
+
+ expose :branch do
+ expose :ref, as: :name
+
+ expose :url do |build|
+ url_to(:namespace_project_tree, build, build.ref)
+ end
+ end
+
+ expose :url do |build|
+ url_to(:namespace_project_build, build)
+ end
+
+ expose :commit_url do |build|
+ url_to(:namespace_project_commit, build, build.sha)
+ end
+
+ private
+
+ def url_to(route, build, id = nil)
+ public_send("#{route}_url", build.project.namespace, build.project, id || build)
+ end
+end
diff --git a/app/serializers/analytics_build_serializer.rb b/app/serializers/analytics_build_serializer.rb
new file mode 100644
index 00000000000..f172d67d356
--- /dev/null
+++ b/app/serializers/analytics_build_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsBuildSerializer < BaseSerializer
+ entity AnalyticsBuildEntity
+end
diff --git a/app/serializers/analytics_commit_entity.rb b/app/serializers/analytics_commit_entity.rb
new file mode 100644
index 00000000000..402cecbfd08
--- /dev/null
+++ b/app/serializers/analytics_commit_entity.rb
@@ -0,0 +1,13 @@
+class AnalyticsCommitEntity < CommitEntity
+ include EntityDateHelper
+
+ expose :short_id, as: :short_sha
+
+ expose :total_time do |commit|
+ distance_of_time_as_hash(request.total_time.to_f)
+ end
+
+ unexpose :author_name
+ unexpose :author_email
+ unexpose :message
+end
diff --git a/app/serializers/analytics_commit_serializer.rb b/app/serializers/analytics_commit_serializer.rb
new file mode 100644
index 00000000000..cdbfecf2b70
--- /dev/null
+++ b/app/serializers/analytics_commit_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsCommitSerializer < BaseSerializer
+ entity AnalyticsCommitEntity
+end
diff --git a/app/serializers/analytics_generic_serializer.rb b/app/serializers/analytics_generic_serializer.rb
new file mode 100644
index 00000000000..9f4859e8410
--- /dev/null
+++ b/app/serializers/analytics_generic_serializer.rb
@@ -0,0 +1,7 @@
+class AnalyticsGenericSerializer < BaseSerializer
+ def represent(resource, opts = {})
+ resource.symbolize_keys!
+
+ super(resource, opts)
+ end
+end
diff --git a/app/serializers/analytics_issue_entity.rb b/app/serializers/analytics_issue_entity.rb
new file mode 100644
index 00000000000..44c50f18613
--- /dev/null
+++ b/app/serializers/analytics_issue_entity.rb
@@ -0,0 +1,29 @@
+class AnalyticsIssueEntity < Grape::Entity
+ include RequestAwareEntity
+ include EntityDateHelper
+
+ expose :title
+ expose :author, using: UserEntity
+
+ expose :iid do |object|
+ object[:iid].to_s
+ end
+
+ expose :total_time do |object|
+ distance_of_time_as_hash(object[:total_time].to_f)
+ end
+
+ expose(:created_at) do |object|
+ interval_in_words(object[:created_at])
+ end
+
+ expose :url do |object|
+ url_to(:namespace_project_issue, id: object[:iid].to_s)
+ end
+
+ private
+
+ def url_to(route, id)
+ public_send("#{route}_url", request.project.namespace, request.project, id)
+ end
+end
diff --git a/app/serializers/analytics_issue_serializer.rb b/app/serializers/analytics_issue_serializer.rb
new file mode 100644
index 00000000000..4fb3e8f1bb4
--- /dev/null
+++ b/app/serializers/analytics_issue_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsIssueSerializer < AnalyticsGenericSerializer
+ entity AnalyticsIssueEntity
+end
diff --git a/app/serializers/analytics_merge_request_entity.rb b/app/serializers/analytics_merge_request_entity.rb
new file mode 100644
index 00000000000..888265eaa38
--- /dev/null
+++ b/app/serializers/analytics_merge_request_entity.rb
@@ -0,0 +1,7 @@
+class AnalyticsMergeRequestEntity < AnalyticsIssueEntity
+ expose :state
+
+ expose :url do |object|
+ url_to(:namespace_project_merge_request, id: object[:iid].to_s)
+ end
+end
diff --git a/app/serializers/analytics_merge_request_serializer.rb b/app/serializers/analytics_merge_request_serializer.rb
new file mode 100644
index 00000000000..4622a1dd855
--- /dev/null
+++ b/app/serializers/analytics_merge_request_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsMergeRequestSerializer < AnalyticsGenericSerializer
+ entity AnalyticsMergeRequestEntity
+end
diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb
index 3d9ac66de0e..b5384e6462b 100644
--- a/app/serializers/build_entity.rb
+++ b/app/serializers/build_entity.rb
@@ -4,21 +4,24 @@ class BuildEntity < Grape::Entity
expose :id
expose :name
- expose :build_url do |build|
- url_to(:namespace_project_build, build)
+ expose :build_path do |build|
+ path_to(:namespace_project_build, build)
end
- expose :retry_url do |build|
- url_to(:retry_namespace_project_build, build)
+ expose :retry_path do |build|
+ path_to(:retry_namespace_project_build, build)
end
- expose :play_url, if: ->(build, _) { build.manual? } do |build|
- url_to(:play_namespace_project_build, build)
+ expose :play_path, if: ->(build, _) { build.manual? } do |build|
+ path_to(:play_namespace_project_build, build)
end
+ expose :created_at
+ expose :updated_at
+
private
- def url_to(route, build)
- send("#{route}_url", build.project.namespace, build.project, build)
+ def path_to(route, build)
+ send("#{route}_path", build.project.namespace, build.project, build)
end
end
diff --git a/app/serializers/commit_entity.rb b/app/serializers/commit_entity.rb
index f7eba6fc1e3..acc20f6dc52 100644
--- a/app/serializers/commit_entity.rb
+++ b/app/serializers/commit_entity.rb
@@ -9,4 +9,11 @@ class CommitEntity < API::Entities::RepoCommit
request.project,
id: commit.id)
end
+
+ expose :commit_path do |commit|
+ namespace_project_tree_path(
+ request.project.namespace,
+ request.project,
+ id: commit.id)
+ end
end
diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb
index ad6fc8d665b..d610fbe0c8a 100644
--- a/app/serializers/deployment_entity.rb
+++ b/app/serializers/deployment_entity.rb
@@ -10,8 +10,8 @@ class DeploymentEntity < Grape::Entity
deployment.ref
end
- expose :ref_url do |deployment|
- namespace_project_tree_url(
+ expose :ref_path do |deployment|
+ namespace_project_tree_path(
deployment.project.namespace,
deployment.project,
id: deployment.ref)
diff --git a/app/serializers/entity_date_helper.rb b/app/serializers/entity_date_helper.rb
new file mode 100644
index 00000000000..9607ad55a8b
--- /dev/null
+++ b/app/serializers/entity_date_helper.rb
@@ -0,0 +1,37 @@
+module EntityDateHelper
+ include ActionView::Helpers::DateHelper
+
+ def interval_in_words(diff)
+ return 'Not started' unless diff
+
+ "#{distance_of_time_in_words(Time.now, diff)} ago"
+ end
+
+ # Converts seconds into a hash such as:
+ # { days: 1, hours: 3, mins: 42, seconds: 40 }
+ #
+ # It returns 0 seconds for zero or negative numbers
+ # It rounds to nearest time unit and does not return zero
+ # i.e { min: 1 } instead of { mins: 1, seconds: 0 }
+ def distance_of_time_as_hash(diff)
+ diff = diff.abs.floor
+
+ return { seconds: 0 } if diff == 0
+
+ mins = (diff / 60).floor
+ seconds = diff % 60
+ hours = (mins / 60).floor
+ mins = mins % 60
+ days = (hours / 24).floor
+ hours = hours % 24
+
+ duration_hash = {}
+
+ duration_hash[:days] = days if days > 0
+ duration_hash[:hours] = hours if hours > 0
+ duration_hash[:mins] = mins if mins > 0
+ duration_hash[:seconds] = seconds if seconds > 0
+
+ duration_hash
+ end
+end
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index ee4392cc46d..7e0fc9c071e 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -9,8 +9,15 @@ class EnvironmentEntity < Grape::Entity
expose :last_deployment, using: DeploymentEntity
expose :stoppable?
- expose :environment_url do |environment|
- namespace_project_environment_url(
+ expose :environment_path do |environment|
+ namespace_project_environment_path(
+ environment.project.namespace,
+ environment.project,
+ environment)
+ end
+
+ expose :stop_path do |environment|
+ stop_namespace_project_environment_path(
environment.project.namespace,
environment.project,
environment)
diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb
new file mode 100644
index 00000000000..17c9160cb19
--- /dev/null
+++ b/app/serializers/issuable_entity.rb
@@ -0,0 +1,16 @@
+class IssuableEntity < Grape::Entity
+ expose :id
+ expose :iid
+ expose :assignee_id
+ expose :author_id
+ expose :description
+ expose :lock_version
+ expose :milestone_id
+ expose :position
+ expose :state
+ expose :title
+ expose :updated_by_id
+ expose :created_at
+ expose :updated_at
+ expose :deleted_at
+end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
new file mode 100644
index 00000000000..6429159ebe1
--- /dev/null
+++ b/app/serializers/issue_entity.rb
@@ -0,0 +1,9 @@
+class IssueEntity < IssuableEntity
+ expose :branch_name
+ expose :confidential
+ expose :due_date
+ expose :moved_to_id
+ expose :project_id
+ expose :milestone, using: API::Entities::Milestone
+ expose :labels, using: LabelEntity
+end
diff --git a/app/serializers/issue_serializer.rb b/app/serializers/issue_serializer.rb
new file mode 100644
index 00000000000..4fff54a9126
--- /dev/null
+++ b/app/serializers/issue_serializer.rb
@@ -0,0 +1,3 @@
+class IssueSerializer < BaseSerializer
+ entity IssueEntity
+end
diff --git a/app/serializers/label_entity.rb b/app/serializers/label_entity.rb
new file mode 100644
index 00000000000..304fd9de08f
--- /dev/null
+++ b/app/serializers/label_entity.rb
@@ -0,0 +1,11 @@
+class LabelEntity < Grape::Entity
+ expose :id
+ expose :title
+ expose :color
+ expose :description
+ expose :group_id
+ expose :project_id
+ expose :template
+ expose :created_at
+ expose :updated_at
+end
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
new file mode 100644
index 00000000000..7445298c714
--- /dev/null
+++ b/app/serializers/merge_request_entity.rb
@@ -0,0 +1,14 @@
+class MergeRequestEntity < IssuableEntity
+ expose :in_progress_merge_commit_sha
+ expose :locked_at
+ expose :merge_commit_sha
+ expose :merge_error
+ expose :merge_params
+ expose :merge_status
+ expose :merge_user_id
+ expose :merge_when_build_succeeds
+ expose :source_branch
+ expose :source_project_id
+ expose :target_branch
+ expose :target_project_id
+end
diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb
new file mode 100644
index 00000000000..aa6e00dfcb4
--- /dev/null
+++ b/app/serializers/merge_request_serializer.rb
@@ -0,0 +1,3 @@
+class MergeRequestSerializer < BaseSerializer
+ entity MergeRequestEntity
+end
diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb
new file mode 100644
index 00000000000..ddaaed90e5b
--- /dev/null
+++ b/app/services/access_token_validation_service.rb
@@ -0,0 +1,32 @@
+AccessTokenValidationService = Struct.new(:token) do
+ # Results:
+ VALID = :valid
+ EXPIRED = :expired
+ REVOKED = :revoked
+ INSUFFICIENT_SCOPE = :insufficient_scope
+
+ def validate(scopes: [])
+ if token.expired?
+ return EXPIRED
+
+ elsif token.revoked?
+ return REVOKED
+
+ elsif !self.include_any_scope?(scopes)
+ return INSUFFICIENT_SCOPE
+
+ else
+ return VALID
+ end
+ end
+
+ # True if the token's scope contains any of the passed scopes.
+ def include_any_scope?(scopes)
+ if scopes.blank?
+ true
+ else
+ # Check whether the token is allowed access to any of the required scopes.
+ Set.new(scopes).intersection(Set.new(token.scopes)).present?
+ end
+ end
+end
diff --git a/app/services/after_branch_delete_service.rb b/app/services/after_branch_delete_service.rb
new file mode 100644
index 00000000000..227e9ea9c6d
--- /dev/null
+++ b/app/services/after_branch_delete_service.rb
@@ -0,0 +1,21 @@
+##
+# Branch can be deleted either by DeleteBranchService
+# or by GitPushService.
+#
+class AfterBranchDeleteService < BaseService
+ attr_reader :branch_name
+
+ def execute(branch_name)
+ @branch_name = branch_name
+
+ stop_environments
+ end
+
+ private
+
+ def stop_environments
+ Ci::StopEnvironmentsService
+ .new(project, current_user)
+ .execute(branch_name)
+ end
+end
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 8ea88da8a53..c00c5aebf57 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -9,8 +9,8 @@ module Auth
return error('UNAVAILABLE', status: 404, message: 'registry not enabled') unless registry.enabled
- unless current_user || project
- return error('DENIED', status: 403, message: 'access forbidden') unless scope
+ unless scope || current_user || project
+ return error('DENIED', status: 403, message: 'access forbidden')
end
{ token: authorized_token(scope).encoded }
@@ -76,7 +76,7 @@ module Auth
case requested_action
when 'pull'
- requested_project.public? || build_can_pull?(requested_project) || user_can_pull?(requested_project)
+ build_can_pull?(requested_project) || user_can_pull?(requested_project)
when 'push'
build_can_push?(requested_project) || user_can_push?(requested_project)
else
@@ -92,23 +92,23 @@ module Auth
# Build can:
# 1. pull from its own project (for ex. a build)
# 2. read images from dependent projects if creator of build is a team member
- @authentication_abilities.include?(:build_read_container_image) &&
+ has_authentication_ability?(:build_read_container_image) &&
(requested_project == project || can?(current_user, :build_read_container_image, requested_project))
end
def user_can_pull?(requested_project)
- @authentication_abilities.include?(:read_container_image) &&
+ has_authentication_ability?(:read_container_image) &&
can?(current_user, :read_container_image, requested_project)
end
def build_can_push?(requested_project)
# Build can push only to the project from which it originates
- @authentication_abilities.include?(:build_create_container_image) &&
+ has_authentication_ability?(:build_create_container_image) &&
requested_project == project
end
def user_can_push?(requested_project)
- @authentication_abilities.include?(:create_container_image) &&
+ has_authentication_ability?(:create_container_image) &&
can?(current_user, :create_container_image, requested_project)
end
@@ -118,5 +118,9 @@ module Auth
http_status: status
}
end
+
+ def has_authentication_ability?(capability)
+ (@authentication_abilities || []).include?(capability)
+ end
end
end
diff --git a/app/services/chat_names/authorize_user_service.rb b/app/services/chat_names/authorize_user_service.rb
new file mode 100644
index 00000000000..321bf3a9205
--- /dev/null
+++ b/app/services/chat_names/authorize_user_service.rb
@@ -0,0 +1,38 @@
+module ChatNames
+ class AuthorizeUserService
+ include Gitlab::Routing.url_helpers
+
+ def initialize(service, params)
+ @service = service
+ @params = params
+ end
+
+ def execute
+ return unless chat_name_params.values.all?(&:present?)
+
+ token = request_token
+
+ new_profile_chat_name_url(token: token) if token
+ end
+
+ private
+
+ def request_token
+ chat_name_token.store!(chat_name_params)
+ end
+
+ def chat_name_token
+ Gitlab::ChatNameToken.new
+ end
+
+ def chat_name_params
+ {
+ service_id: @service.id,
+ team_id: @params[:team_id],
+ team_domain: @params[:team_domain],
+ chat_id: @params[:user_id],
+ chat_name: @params[:user_name]
+ }
+ end
+ end
+end
diff --git a/app/services/chat_names/find_user_service.rb b/app/services/chat_names/find_user_service.rb
new file mode 100644
index 00000000000..4f5c5567b42
--- /dev/null
+++ b/app/services/chat_names/find_user_service.rb
@@ -0,0 +1,26 @@
+module ChatNames
+ class FindUserService
+ def initialize(service, params)
+ @service = service
+ @params = params
+ end
+
+ def execute
+ chat_name = find_chat_name
+ return unless chat_name
+
+ chat_name.touch(:last_used_at)
+ chat_name.user
+ end
+
+ private
+
+ def find_chat_name
+ ChatName.find_by(
+ service: @service,
+ team_id: @params[:team_id],
+ chat_id: @params[:user_id]
+ )
+ end
+ end
+end
diff --git a/app/services/ci/create_pipeline_builds_service.rb b/app/services/ci/create_pipeline_builds_service.rb
index 005014fa1de..b7da3f8e7eb 100644
--- a/app/services/ci/create_pipeline_builds_service.rb
+++ b/app/services/ci/create_pipeline_builds_service.rb
@@ -10,18 +10,29 @@ module Ci
end
end
+ def project
+ pipeline.project
+ end
+
private
def create_build(build_attributes)
build_attributes = build_attributes.merge(
pipeline: pipeline,
- project: pipeline.project,
+ project: project,
ref: pipeline.ref,
tag: pipeline.tag,
user: current_user,
trigger_request: trigger_request
)
- pipeline.builds.create(build_attributes)
+ build = pipeline.builds.create(build_attributes)
+
+ # Create the environment before the build starts. This sets its slug and
+ # makes it available as an environment variable
+ project.environments.find_or_create_by(name: build.expanded_environment_name) if
+ build.has_environment?
+
+ build
end
def new_builds
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index cde856b0186..e3bc9847200 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -45,9 +45,15 @@ module Ci
return error('No builds for this pipeline.')
end
- pipeline.save
- pipeline.process!
- pipeline
+ Ci::Pipeline.transaction do
+ pipeline.save
+
+ Ci::CreatePipelineBuildsService
+ .new(project, current_user)
+ .execute(pipeline)
+ end
+
+ pipeline.tap(&:process!)
end
private
diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb
index 75d847d5bee..240ddabec36 100644
--- a/app/services/ci/image_for_build_service.rb
+++ b/app/services/ci/image_for_build_service.rb
@@ -1,13 +1,13 @@
module Ci
class ImageForBuildService
def execute(project, opts)
- sha = opts[:sha] || ref_sha(project, opts[:ref])
-
+ ref = opts[:ref]
+ sha = opts[:sha] || ref_sha(project, ref)
pipelines = project.pipelines.where(sha: sha)
- pipelines = pipelines.where(ref: opts[:ref]) if opts[:ref]
- image_name = image_for_status(pipelines.status)
+ image_name = image_for_status(pipelines.latest_status(ref))
image_path = Rails.root.join('public/ci', image_name)
+
OpenStruct.new(path: image_path, name: image_name)
end
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 8face432d97..79eb97b7b55 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -5,10 +5,7 @@ module Ci
def execute(pipeline)
@pipeline = pipeline
- # This method will ensure that our pipeline does have all builds for all stages created
- if created_builds.empty?
- create_builds!
- end
+ ensure_created_builds! # TODO, remove me in 9.0
new_builds =
stage_indexes_of_created_builds.map do |index|
@@ -22,10 +19,6 @@ module Ci
private
- def create_builds!
- Ci::CreatePipelineBuildsService.new(project, current_user).execute(pipeline)
- end
-
def process_stage(index)
current_status = status_for_prior_stages(index)
@@ -51,11 +44,11 @@ module Ci
def valid_statuses_for_when(value)
case value
when 'on_success'
- %w[success]
+ %w[success skipped]
when 'on_failure'
%w[failed]
when 'always'
- %w[success failed]
+ %w[success failed skipped]
else
[]
end
@@ -76,5 +69,18 @@ module Ci
def created_builds
pipeline.builds.created
end
+
+ # This method is DEPRECATED and should be removed in 9.0.
+ #
+ # We need it to maintain backwards compatibility with previous versions
+ # when builds were not created within one transaction with the pipeline.
+ #
+ def ensure_created_builds!
+ return if created_builds.any?
+
+ Ci::CreatePipelineBuildsService
+ .new(project, current_user)
+ .execute(pipeline)
+ end
end
end
diff --git a/app/services/ci/send_pipeline_notification_service.rb b/app/services/ci/send_pipeline_notification_service.rb
deleted file mode 100644
index ceb182801f7..00000000000
--- a/app/services/ci/send_pipeline_notification_service.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module Ci
- class SendPipelineNotificationService
- attr_reader :pipeline
-
- def initialize(new_pipeline)
- @pipeline = new_pipeline
- end
-
- def execute(recipients)
- email_template = "pipeline_#{pipeline.status}_email"
-
- return unless Notify.respond_to?(email_template)
-
- recipients.each do |to|
- Notify.public_send(email_template, pipeline, to).deliver_later
- end
- end
- end
-end
diff --git a/app/services/ci/stop_environments_service.rb b/app/services/ci/stop_environments_service.rb
new file mode 100644
index 00000000000..cf590459cb2
--- /dev/null
+++ b/app/services/ci/stop_environments_service.rb
@@ -0,0 +1,29 @@
+module Ci
+ class StopEnvironmentsService < BaseService
+ attr_reader :ref
+
+ def execute(branch_name)
+ @ref = branch_name
+
+ return unless has_ref?
+
+ environments.each do |environment|
+ next unless environment.stoppable?
+ next unless can?(current_user, :create_deployment, project)
+
+ environment.stop!(current_user)
+ end
+ end
+
+ private
+
+ def has_ref?
+ @ref.present?
+ end
+
+ def environments
+ @environments ||= project
+ .environments_recently_updated_on_branch(@ref)
+ end
+ end
+end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index 1c82599c579..4d410f66c55 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -34,8 +34,8 @@ module Commits
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} automatically.
- It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content."
+ error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
+ A #{action.to_s.dasherize} may have already been performed with this #{@commit.change_type_title(current_user)}, or a more recent commit may have updated some of its content."
raise ChangeError, error_msg
end
end
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index 757fc35a78f..e004a303496 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -1,5 +1,3 @@
-require_relative 'base_service'
-
class CreateBranchService < BaseService
def execute(branch_name, ref, source_project: @project)
valid_branch = Gitlab::GitRefValidator.validate(branch_name)
diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb
index 8ae15ad32f4..47f9b2c621c 100644
--- a/app/services/create_deployment_service.rb
+++ b/app/services/create_deployment_service.rb
@@ -1,5 +1,3 @@
-require_relative 'base_service'
-
class CreateDeploymentService < BaseService
def execute(deployable = nil)
return unless executable?
diff --git a/app/services/create_release_service.rb b/app/services/create_release_service.rb
index d6d4afcf29a..54ff1f74126 100644
--- a/app/services/create_release_service.rb
+++ b/app/services/create_release_service.rb
@@ -1,5 +1,3 @@
-require_relative 'base_service'
-
class CreateReleaseService < BaseService
def execute(tag_name, release_description)
repository = project.repository
diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb
index c0e7ecf6a96..fe9353afeb8 100644
--- a/app/services/create_tag_service.rb
+++ b/app/services/create_tag_service.rb
@@ -1,5 +1,3 @@
-require_relative 'base_service'
-
class CreateTagService < BaseService
def execute(tag_name, target, message, release_description = nil)
valid_tag = Gitlab::GitRefValidator.validate(tag_name)
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index 3e5dd4ebb86..11a045f4c31 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -1,5 +1,3 @@
-require_relative 'base_service'
-
class DeleteBranchService < BaseService
def execute(branch_name)
repository = project.repository
diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb
new file mode 100644
index 00000000000..1b5623baebe
--- /dev/null
+++ b/app/services/delete_merged_branches_service.rb
@@ -0,0 +1,16 @@
+class DeleteMergedBranchesService < BaseService
+ def async_execute
+ DeleteMergedBranchesWorker.perform_async(project.id, current_user.id)
+ end
+
+ def execute
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :push_code, project)
+
+ branches = project.repository.branch_names
+ branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) }
+
+ branches.each do |branch|
+ DeleteBranchService.new(project, current_user).execute(branch)
+ end
+ end
+end
diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb
index d824406cb49..a44dee14a0f 100644
--- a/app/services/delete_tag_service.rb
+++ b/app/services/delete_tag_service.rb
@@ -1,5 +1,3 @@
-require_relative 'base_service'
-
class DeleteTagService < BaseService
def execute(tag_name)
repository = project.repository
diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb
index 0081364b8aa..2316c57bf1e 100644
--- a/app/services/destroy_group_service.rb
+++ b/app/services/destroy_group_service.rb
@@ -6,12 +6,10 @@ class DestroyGroupService
end
def async_execute
- group.transaction do
- # Soft delete via paranoia gem
- group.destroy
- job_id = GroupDestroyWorker.perform_async(group.id, current_user.id)
- Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}")
- end
+ # Soft delete via paranoia gem
+ group.destroy
+ job_id = GroupDestroyWorker.perform_async(group.id, current_user.id)
+ Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}")
end
def execute
@@ -22,6 +20,10 @@ class DestroyGroupService
::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/discussions/base_service.rb b/app/services/discussions/base_service.rb
new file mode 100644
index 00000000000..e4dfe6e71bb
--- /dev/null
+++ b/app/services/discussions/base_service.rb
@@ -0,0 +1,4 @@
+module Discussions
+ class BaseService < ::BaseService
+ end
+end
diff --git a/app/services/discussions/resolve_service.rb b/app/services/discussions/resolve_service.rb
new file mode 100644
index 00000000000..0437195f588
--- /dev/null
+++ b/app/services/discussions/resolve_service.rb
@@ -0,0 +1,24 @@
+module Discussions
+ class ResolveService < Discussions::BaseService
+ def execute(one_or_more_discussions)
+ Array(one_or_more_discussions).each { |discussion| resolve_discussion(discussion) }
+ end
+
+ def resolve_discussion(discussion)
+ return unless discussion.can_resolve?(current_user)
+
+ discussion.resolve!(current_user)
+
+ MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request)
+ SystemNoteService.discussion_continued_in_issue(discussion, project, current_user, follow_up_issue) if follow_up_issue
+ end
+
+ def merge_request
+ params[:merge_request]
+ end
+
+ def follow_up_issue
+ params[:follow_up_issue]
+ end
+ end
+end
diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb
index d00d78cee7e..e5b4d60e467 100644
--- a/app/services/files/create_dir_service.rb
+++ b/app/services/files/create_dir_service.rb
@@ -1,5 +1,3 @@
-require_relative "base_service"
-
module Files
class CreateDirService < Files::BaseService
def commit
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index bf127843d55..b23576b9a28 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -1,5 +1,3 @@
-require_relative "base_service"
-
module Files
class CreateService < Files::BaseService
def commit
diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb
index 8b27ad51789..4f7e7a5baaa 100644
--- a/app/services/files/delete_service.rb
+++ b/app/services/files/delete_service.rb
@@ -1,5 +1,3 @@
-require_relative "base_service"
-
module Files
class DeleteService < Files::BaseService
def commit
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index d28912e1301..54446e90007 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -1,5 +1,3 @@
-require_relative "base_service"
-
module Files
class MultiService < Files::BaseService
class FileChangedError < StandardError; end
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index c17fdb8d1f1..47a18e3e132 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -1,5 +1,3 @@
-require_relative "base_service"
-
module Files
class UpdateService < Files::BaseService
class FileChangedError < StandardError; end
diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb
index 172bd85dade..6cd3908d43a 100644
--- a/app/services/git_hooks_service.rb
+++ b/app/services/git_hooks_service.rb
@@ -1,6 +1,8 @@
class GitHooksService
PreReceiveError = Class.new(StandardError)
+ attr_accessor :oldrev, :newrev, :ref
+
def execute(user, repo_path, oldrev, newrev, ref)
@repo_path = repo_path
@user = Gitlab::GlId.gl_id(user)
@@ -16,7 +18,7 @@ class GitHooksService
end
end
- yield
+ yield self
run_hook('post-receive')
end
@@ -25,6 +27,6 @@ class GitHooksService
def run_hook(name)
hook = Gitlab::Git::Hook.new(name, @repo_path)
- hook.trigger(@user, @oldrev, @newrev, @ref)
+ hook.trigger(@user, oldrev, newrev, ref)
end
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index de313095bed..185556c12cc 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -18,7 +18,7 @@ class GitPushService < BaseService
#
def execute
@project.repository.after_create if @project.empty_repo?
- @project.repository.after_push_commit(branch_name, params[:newrev])
+ @project.repository.after_push_commit(branch_name)
if push_remove_branch?
@project.repository.after_remove_branch
@@ -49,27 +49,53 @@ class GitPushService < BaseService
update_gitattributes if is_default_branch?
end
- # Update merge requests that may be affected by this push. A new branch
- # could cause the last commit of a merge request to change.
- update_merge_requests
-
+ execute_related_hooks
perform_housekeeping
+
+ update_caches
end
def update_gitattributes
@project.repository.copy_gitattributes(params[:ref])
end
+ def update_caches
+ if is_default_branch?
+ paths = Set.new
+
+ @push_commits.each do |commit|
+ commit.raw_diffs(deltas_only: true).each do |diff|
+ paths << diff.new_path
+ end
+ end
+
+ types = Gitlab::FileDetector.types_in_paths(paths.to_a)
+ else
+ types = []
+ end
+
+ ProjectCacheWorker.perform_async(@project.id, types)
+ end
+
protected
- def update_merge_requests
- UpdateMergeRequestsWorker.perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref])
+ def execute_related_hooks
+ # Update merge requests that may be affected by this push. A new branch
+ # could cause the last commit of a merge request to change.
+ #
+ UpdateMergeRequestsWorker
+ .perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref])
EventCreateService.new.push(@project, current_user, build_push_data)
@project.execute_hooks(build_push_data.dup, :push_hooks)
@project.execute_services(build_push_data.dup, :push_hooks)
Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute
- ProjectCacheWorker.perform_async(@project.id)
+
+ if push_remove_branch?
+ AfterBranchDeleteService
+ .new(project, current_user)
+ .execute(branch_name)
+ end
end
def perform_housekeeping
@@ -109,7 +135,7 @@ class GitPushService < BaseService
@push_commits.each do |commit|
ProcessCommitWorker.
- perform_async(project.id, current_user.id, commit.id, default)
+ perform_async(project.id, current_user.id, commit.to_hash, default)
end
end
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index 99ad12b1003..fff2273f402 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -5,7 +5,7 @@ module Groups
new_visibility = params[:visibility_level]
if new_visibility && new_visibility.to_i != group.visibility_level
unless can?(current_user, :change_visibility_level, group) &&
- Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
+ Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(group, new_visibility)
return group
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index bb92cd80cc9..ab3d2a9a0cd 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -85,14 +85,15 @@ class IssuableBaseService < BaseService
def find_or_create_label_ids
labels = params.delete(:labels)
+
return unless labels
- params[:label_ids] = labels.split(',').map do |label_name|
+ params[:label_ids] = labels.split(",").map do |label_name|
service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip)
label = service.execute
- label.id
- end
+ label.try(:id)
+ end.compact
end
def process_label_ids(attributes, existing_label_ids: nil)
@@ -119,9 +120,10 @@ class IssuableBaseService < BaseService
def merge_slash_commands_into_params!(issuable)
description, command_params =
SlashCommands::InterpretService.new(project, current_user).
- execute(params[:description], issuable)
+ execute(params[:description], issuable)
- params[:description] = description
+ # Avoid a description already set on an issuable to be overwritten by a nil
+ params[:description] = description if params.has_key?(:description)
params.merge!(command_params)
end
@@ -140,6 +142,7 @@ class IssuableBaseService < BaseService
params.delete(:state_event)
params[:author] ||= current_user
+
label_ids = process_label_ids(params)
issuable.assign_attributes(params)
@@ -181,11 +184,10 @@ class IssuableBaseService < BaseService
old_labels = issuable.labels.to_a
old_mentioned_users = issuable.mentioned_users.to_a
- params[:label_ids] = process_label_ids(params, existing_label_ids: issuable.label_ids)
+ label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
+ params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
if params.present? && update_issuable(issuable, params)
- issuable.reset_events_cache
-
# We do not touch as it will affect a update on updated_at field
ActiveRecord::Base.no_touching do
handle_common_system_notes(issuable, old_labels: old_labels)
@@ -200,6 +202,10 @@ class IssuableBaseService < BaseService
issuable
end
+ def labels_changing?(old_label_ids, new_label_ids)
+ old_label_ids.sort != new_label_ids.sort
+ end
+
def change_state(issuable)
case params.delete(:state_event)
when 'reopen'
@@ -212,9 +218,9 @@ class IssuableBaseService < BaseService
def change_subscription(issuable)
case params.delete(:subscription_event)
when 'subscribe'
- issuable.subscribe(current_user)
+ issuable.subscribe(current_user, project)
when 'unsubscribe'
- issuable.unsubscribe(current_user)
+ issuable.unsubscribe(current_user, project)
end
end
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 9ea3ce084ba..742e834df97 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -1,5 +1,13 @@
module Issues
class BaseService < ::IssuableBaseService
+ attr_reader :merge_request_for_resolving_discussions
+
+ def initialize(*args)
+ super
+
+ @merge_request_for_resolving_discussions ||= params.delete(:merge_request_for_resolving_discussions)
+ end
+
def hook_data(issue, action)
issue_data = issue.to_hook_data(current_user)
issue_url = Gitlab::UrlBuilder.build(issue)
diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb
new file mode 100644
index 00000000000..a63982f60c8
--- /dev/null
+++ b/app/services/issues/build_service.rb
@@ -0,0 +1,50 @@
+module Issues
+ class BuildService < Issues::BaseService
+ def execute
+ @issue = project.issues.new(issue_params)
+ end
+
+ def issue_params_with_info_from_merge_request
+ return {} unless merge_request_for_resolving_discussions
+
+ { title: title_from_merge_request, description: description_from_merge_request }
+ end
+
+ def title_from_merge_request
+ "Follow-up from \"#{merge_request_for_resolving_discussions.title}\""
+ end
+
+ def description_from_merge_request
+ if merge_request_for_resolving_discussions.resolvable_discussions.empty?
+ return "There are no unresolved discussions. "\
+ "Review the conversation in #{merge_request_for_resolving_discussions.to_reference}"
+ end
+
+ description = "The following discussions from #{merge_request_for_resolving_discussions.to_reference} should be addressed:"
+ [description, *items_for_discussions].join("\n\n")
+ end
+
+ def items_for_discussions
+ merge_request_for_resolving_discussions.resolvable_discussions.map { |discussion| item_for_discussion(discussion) }
+ end
+
+ def item_for_discussion(discussion)
+ first_note = discussion.first_note_to_resolve
+ other_note_count = discussion.notes.size - 1
+ creation_time = first_note.created_at.to_s(:medium)
+ note_url = Gitlab::UrlBuilder.build(first_note)
+
+ discussion_info = "- [ ] #{first_note.author.to_reference} commented in a discussion on [#{creation_time}](#{note_url}): "
+ discussion_info << " (+#{other_note_count} #{'comment'.pluralize(other_note_count)})" if other_note_count > 0
+
+ note_without_block_quotes = Banzai::Filter::BlockquoteFenceFilter.new(first_note.note).call
+ quote = ">>>\n#{note_without_block_quotes}\n>>>"
+
+ [discussion_info, quote].join("\n\n")
+ end
+
+ def issue_params
+ @issue_params ||= issue_params_with_info_from_merge_request.merge(params.slice(:title, :description))
+ end
+ end
+end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index ab4c51386a4..f1030912c68 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -17,7 +17,7 @@ module Issues
# allowed to close the given issue.
def close_issue(issue, commit: nil, notifications: true, system_note: true)
if project.jira_tracker? && project.jira_service.active
- project.jira_service.execute(commit, issue)
+ project.jira_service.close_issue(commit, issue)
todo_service.close_issue(issue, current_user)
return issue
end
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index ea1690f3e38..d2eb46ac41b 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -4,7 +4,8 @@ module Issues
@request = params.delete(:request)
@api = params.delete(:api)
- @issue = project.issues.new
+ issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
+ @issue = BuildService.new(project, current_user, issue_attributes).execute
create(@issue)
end
@@ -18,6 +19,17 @@ module Issues
notification_service.new_issue(issuable, current_user)
todo_service.new_issue(issuable, current_user)
user_agent_detail_service.create
+
+ if merge_request_for_resolving_discussions.try(:discussions_can_be_resolved_by?, current_user)
+ resolve_discussions_in_merge_request(issuable)
+ end
+ end
+
+ def resolve_discussions_in_merge_request(issue)
+ Discussions::ResolveService.new(project, current_user,
+ merge_request: merge_request_for_resolving_discussions,
+ follow_up_issue: issue).
+ execute(merge_request_for_resolving_discussions.resolvable_discussions)
end
private
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index a2111b3806b..78cbf94ec69 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -10,7 +10,7 @@ module Issues
end
if issue.previous_changes.include?('title') ||
- issue.previous_changes.include?('description')
+ issue.previous_changes.include?('description')
todo_service.update_issue(issue, current_user)
end
diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb
index d622f9edd33..cf4f7606c94 100644
--- a/app/services/labels/find_or_create_service.rb
+++ b/app/services/labels/find_or_create_service.rb
@@ -22,9 +22,14 @@ module Labels
).execute(skip_authorization: skip_authorization)
end
+ # Only creates the label if current_user can do so, if the label does not exist
+ # and the user can not create the label, nil is returned
def find_or_create_label
new_label = available_labels.find_by(title: title)
- new_label ||= project.labels.create(params)
+
+ if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project))
+ new_label = project.labels.create(params)
+ end
new_label
end
diff --git a/app/services/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb
index d572a928a42..12a8415d9a5 100644
--- a/app/services/merge_requests/add_todo_when_build_fails_service.rb
+++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb
@@ -1,13 +1,18 @@
module MergeRequests
class AddTodoWhenBuildFailsService < MergeRequests::BaseService
# Adds a todo to the parent merge_request when a CI build fails
+ #
def execute(commit_status)
+ return if commit_status.allow_failure?
+
commit_status_merge_requests(commit_status) do |merge_request|
todo_service.merge_request_build_failed(merge_request)
end
end
- # Closes any pending build failed todos for the parent MRs when a build is retried
+ # Closes any pending build failed todos for the parent MRs when a
+ # build is retried
+ #
def close(commit_status)
commit_status_merge_requests(commit_status) do |merge_request|
todo_service.merge_request_build_retried(merge_request)
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 58f69a41e14..800fd39c424 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -55,7 +55,7 @@ module MergeRequests
def pipeline_merge_requests(pipeline)
merge_requests_for(pipeline.ref).each do |merge_request|
- next unless pipeline == merge_request.pipeline
+ next unless pipeline == merge_request.head_pipeline
yield merge_request
end
@@ -63,7 +63,7 @@ module MergeRequests
def commit_status_merge_requests(commit_status)
merge_requests_for(commit_status.ref).each do |merge_request|
- pipeline = merge_request.pipeline
+ pipeline = merge_request.head_pipeline
next unless pipeline
next unless pipeline.sha == commit_status.sha
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index f415244068b..6a7393a9921 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -42,17 +42,17 @@ module MergeRequests
end
if merge_request.source_project == merge_request.target_project &&
- merge_request.target_branch == merge_request.source_branch
+ merge_request.target_branch == merge_request.source_branch
messages << 'You must select different branches'
end
# See if source and target branches exist
- unless merge_request.source_project.commit(merge_request.source_branch)
+ if merge_request.source_branch.present? && !merge_request.source_project.commit(merge_request.source_branch)
messages << "Source branch \"#{merge_request.source_branch}\" does not exist"
end
- unless merge_request.target_project.commit(merge_request.target_branch)
+ if merge_request.target_branch.present? && !merge_request.target_project.commit(merge_request.target_branch)
messages << "Target branch \"#{merge_request.target_branch}\" does not exist"
end
@@ -81,7 +81,7 @@ module MergeRequests
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)) && !issue.try(:confidential?)
+ elsif iid && issue = merge_request.target_project.get_issue(iid, current_user)
case issue
when Issue
merge_request.title = "Resolve \"#{issue.title}\""
diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb
deleted file mode 100644
index dc159de0058..00000000000
--- a/app/services/merge_requests/merge_when_build_succeeds_service.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-module MergeRequests
- class MergeWhenBuildSucceedsService < MergeRequests::BaseService
- # Marks the passed `merge_request` to be merged when the build succeeds or
- # updates the params for the automatic merge
- def execute(merge_request)
- merge_request.merge_params.merge!(params)
-
- # The service is also called when the merge params are updated.
- already_approved = merge_request.merge_when_build_succeeds?
-
- unless already_approved
- merge_request.merge_when_build_succeeds = true
- merge_request.merge_user = @current_user
-
- SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.diff_head_commit)
- end
-
- merge_request.save
- end
-
- # Triggers the automatic merge of merge_request once the pipeline succeeds
- def trigger(pipeline)
- return unless pipeline.success?
-
- pipeline_merge_requests(pipeline) do |merge_request|
- next unless merge_request.merge_when_build_succeeds?
- next unless merge_request.mergeable?
-
- MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
- end
- end
-
- # Cancels the automatic merge
- def cancel(merge_request)
- if merge_request.merge_when_build_succeeds? && merge_request.open?
- merge_request.reset_merge_when_build_succeeds
- SystemNoteService.cancel_merge_when_build_succeeds(merge_request, @project, @current_user)
-
- success
- else
- error("Can't cancel the automatic merge", 406)
- end
- end
- end
-end
diff --git a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
new file mode 100644
index 00000000000..5616edf8b4a
--- /dev/null
+++ b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
@@ -0,0 +1,45 @@
+module MergeRequests
+ class MergeWhenPipelineSucceedsService < MergeRequests::BaseService
+ # Marks the passed `merge_request` to be merged when the build succeeds or
+ # updates the params for the automatic merge
+ def execute(merge_request)
+ merge_request.merge_params.merge!(params)
+
+ # The service is also called when the merge params are updated.
+ already_approved = merge_request.merge_when_build_succeeds?
+
+ unless already_approved
+ merge_request.merge_when_build_succeeds = true
+ merge_request.merge_user = @current_user
+
+ SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.diff_head_commit)
+ end
+
+ merge_request.save
+ end
+
+ # Triggers the automatic merge of merge_request once the pipeline succeeds
+ def trigger(pipeline)
+ return unless pipeline.success?
+
+ pipeline_merge_requests(pipeline) do |merge_request|
+ next unless merge_request.merge_when_build_succeeds?
+ next unless merge_request.mergeable?
+
+ MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
+ end
+ end
+
+ # Cancels the automatic merge
+ def cancel(merge_request)
+ if merge_request.merge_when_build_succeeds? && merge_request.open?
+ merge_request.reset_merge_when_build_succeeds
+ SystemNoteService.cancel_merge_when_build_succeeds(merge_request, @project, @current_user)
+
+ success
+ else
+ error("Can't cancel the automatic merge", 406)
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 22596b4014a..0a9563ed7e7 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -55,15 +55,16 @@ module MergeRequests
# Refresh merge request diff if we push to source or target branch of merge request
# Note: we should update merge requests from forks too
def reload_merge_requests
- merge_requests = @project.merge_requests.opened.by_branch(@branch_name).to_a
- merge_requests += fork_merge_requests.by_branch(@branch_name).to_a
+ merge_requests = @project.merge_requests.opened.
+ by_source_or_target_branch(@branch_name).to_a
+ merge_requests += fork_merge_requests
merge_requests = filter_merge_requests(merge_requests)
merge_requests.each do |merge_request|
if merge_request.source_branch == @branch_name || force_push?
merge_request.reload_diff
else
- mr_commit_ids = merge_request.commits.map(&:id)
+ mr_commit_ids = merge_request.commits_sha
push_commit_ids = @commits.map(&:id)
matches = mr_commit_ids & push_commit_ids
merge_request.reload_diff if matches.any?
@@ -123,7 +124,7 @@ module MergeRequests
return unless @commits.present?
merge_requests_for_source_branch.each do |merge_request|
- mr_commit_ids = Set.new(merge_request.commits.map(&:id))
+ mr_commit_ids = Set.new(merge_request.commits_sha)
new_commits, existing_commits = @commits.partition do |commit|
mr_commit_ids.include?(commit.id)
@@ -157,13 +158,14 @@ module MergeRequests
def merge_requests_for_source_branch
@source_merge_requests ||= begin
merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a
- merge_requests += fork_merge_requests.where(source_branch: @branch_name).to_a
+ merge_requests += fork_merge_requests
filter_merge_requests(merge_requests)
end
end
def fork_merge_requests
- @fork_merge_requests ||= @project.fork_merge_requests.opened
+ @fork_merge_requests ||= @project.fork_merge_requests.opened.
+ where(source_branch: @branch_name).to_a
end
def branch_added?
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index a37cc3fdf21..ad16ef8c70f 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -1,7 +1,3 @@
-require_relative 'base_service'
-require_relative 'reopen_service'
-require_relative 'close_service'
-
module MergeRequests
class UpdateService < MergeRequests::BaseService
def execute(merge_request)
@@ -29,7 +25,7 @@ module MergeRequests
end
if merge_request.previous_changes.include?('title') ||
- merge_request.previous_changes.include?('description')
+ merge_request.previous_changes.include?('description')
todo_service.update_merge_request(merge_request, current_user)
end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 723cc0e6834..d75592e31f3 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -26,13 +26,16 @@ module Notes
note.note = content
end
- if !only_commands && note.save
+ note.run_after_commit do
# Finish the harder work in the background
- NewNoteWorker.perform_in(2.seconds, note.id, params)
+ NewNoteWorker.perform_async(note.id)
+ end
+
+ if !only_commands && note.save
todo_service.new_note(note, current_user)
end
- if command_params && command_params.any?
+ if command_params.present?
slash_commands_service.execute(command_params, note)
# We must add the error after we call #save because errors are reset
@@ -40,6 +43,8 @@ module Notes
if only_commands
note.errors.add(:commands_only, 'Your commands have been executed!')
end
+
+ note.commands_changes = command_params.keys
end
note
diff --git a/app/services/notes/delete_service.rb b/app/services/notes/delete_service.rb
index 7f1b30ec84e..a673e8e9dde 100644
--- a/app/services/notes/delete_service.rb
+++ b/app/services/notes/delete_service.rb
@@ -2,7 +2,6 @@ module Notes
class DeleteService < BaseService
def execute(note)
note.destroy
- note.reset_events_cache
end
end
end
diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb
index 1361b1e0300..75a4b3ed826 100644
--- a/app/services/notes/update_service.rb
+++ b/app/services/notes/update_service.rb
@@ -5,7 +5,6 @@ module Notes
note.update_attributes(params.merge(updated_by: current_user))
note.create_new_cross_references!(current_user)
- note.reset_events_cache
if note.previous_changes.include?('note')
TodoService.new.update_note(note, current_user)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 72712afc07e..9a7af5730d2 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -75,7 +75,7 @@ class NotificationService
# * watchers of the issue's labels
#
def relabeled_issue(issue, added_labels, current_user)
- relabeled_resource_email(issue, added_labels, current_user, :relabeled_issue_email)
+ relabeled_resource_email(issue, issue.project, added_labels, current_user, :relabeled_issue_email)
end
# When create a merge request we should send an email to:
@@ -118,7 +118,7 @@ class NotificationService
# * watchers of the mr's labels
#
def relabeled_merge_request(merge_request, added_labels, current_user)
- relabeled_resource_email(merge_request, added_labels, current_user, :relabeled_merge_request_email)
+ relabeled_resource_email(merge_request, merge_request.target_project, added_labels, current_user, :relabeled_merge_request_email)
end
def close_mr(merge_request, current_user)
@@ -171,7 +171,6 @@ class NotificationService
return true unless note.noteable_type.present?
# ignore gitlab service messages
- return true if note.note.start_with?('Status changed to closed')
return true if note.cross_reference? && note.system?
target = note.noteable
@@ -205,7 +204,7 @@ class NotificationService
recipients = reject_muted_users(recipients, note.project)
- recipients = add_subscribed_users(recipients, note.noteable)
+ recipients = add_subscribed_users(recipients, note.project, note.noteable)
recipients = reject_unsubscribed_users(recipients, note.noteable)
recipients = reject_users_without_access(recipients, note.noteable)
@@ -312,6 +311,22 @@ class NotificationService
mailer.project_was_not_exported_email(current_user, project, errors).deliver_later
end
+ def pipeline_finished(pipeline, recipients = nil)
+ email_template = "pipeline_#{pipeline.status}_email"
+
+ return unless mailer.respond_to?(email_template)
+
+ recipients ||= build_recipients(
+ pipeline,
+ pipeline.project,
+ nil, # The acting user, who won't be added to recipients
+ action: pipeline.status).map(&:notification_email)
+
+ if recipients.any?
+ mailer.public_send(email_template, pipeline, recipients).deliver_later
+ end
+ end
+
protected
# Get project/group users with CUSTOM notification level
@@ -377,7 +392,7 @@ class NotificationService
)
end
- # Build a list of users based on project notifcation settings
+ # Build a list of users based on project notification settings
def select_project_member_setting(project, global_setting, users_global_level_watch)
users = notification_settings_for(project, :watch)
@@ -475,26 +490,31 @@ class NotificationService
end
def reject_users_without_access(recipients, target)
- return recipients unless target.is_a?(Issuable)
+ ability = case target
+ when Issuable
+ :"read_#{target.to_ability_name}"
+ when Ci::Pipeline
+ :read_build # We have build trace in pipeline emails
+ end
- ability = :"read_#{target.to_ability_name}"
+ return recipients unless ability
recipients.select do |user|
user.can?(ability, target)
end
end
- def add_subscribed_users(recipients, target)
+ def add_subscribed_users(recipients, project, target)
return recipients unless target.respond_to? :subscribers
- recipients + target.subscribers
+ recipients + target.subscribers(project)
end
- def add_labels_subscribers(recipients, target, labels: nil)
+ def add_labels_subscribers(recipients, project, target, labels: nil)
return recipients unless target.respond_to? :labels
(labels || target.labels).each do |label|
- recipients += label.subscribers
+ recipients += label.subscribers(project)
end
recipients
@@ -550,8 +570,8 @@ class NotificationService
end
end
- def relabeled_resource_email(target, labels, current_user, method)
- recipients = build_relabeled_recipients(target, current_user, labels: labels)
+ def relabeled_resource_email(target, project, labels, current_user, method)
+ recipients = build_relabeled_recipients(target, project, current_user, labels: labels)
label_names = labels.map(&:name)
recipients.each do |recipient|
@@ -587,10 +607,10 @@ class NotificationService
end
recipients = reject_muted_users(recipients, project)
- recipients = add_subscribed_users(recipients, target)
+ recipients = add_subscribed_users(recipients, project, target)
if [:new_issue, :new_merge_request].include?(custom_action)
- recipients = add_labels_subscribers(recipients, target)
+ recipients = add_labels_subscribers(recipients, project, target)
end
recipients = reject_unsubscribed_users(recipients, target)
@@ -601,8 +621,8 @@ class NotificationService
recipients.uniq
end
- def build_relabeled_recipients(target, current_user, labels:)
- recipients = add_labels_subscribers([], target, labels: labels)
+ def build_relabeled_recipients(target, project, current_user, labels:)
+ recipients = add_labels_subscribers([], project, target, labels: labels)
recipients = reject_unsubscribed_users(recipients, target)
recipients = reject_users_without_access(recipients, target)
recipients.delete(current_user)
@@ -624,6 +644,6 @@ class NotificationService
# Build event key to search on custom notification level
# Check NotificationSetting::EMAIL_EVENTS
def build_custom_key(action, object)
- "#{action}_#{object.class.name.underscore}".to_sym
+ "#{action}_#{object.class.model_name.name.underscore}".to_sym
end
end
diff --git a/app/services/oauth2/access_token_validation_service.rb b/app/services/oauth2/access_token_validation_service.rb
deleted file mode 100644
index 264fdccde8f..00000000000
--- a/app/services/oauth2/access_token_validation_service.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-module Oauth2::AccessTokenValidationService
- # Results:
- VALID = :valid
- EXPIRED = :expired
- REVOKED = :revoked
- INSUFFICIENT_SCOPE = :insufficient_scope
-
- class << self
- def validate(token, scopes: [])
- if token.expired?
- return EXPIRED
-
- elsif token.revoked?
- return REVOKED
-
- elsif !self.sufficient_scope?(token, scopes)
- return INSUFFICIENT_SCOPE
-
- else
- return VALID
- end
- end
-
- protected
-
- # True if the token's scope is a superset of required scopes,
- # or the required scopes is empty.
- def sufficient_scope?(token, scopes)
- if scopes.blank?
- # if no any scopes required, the scopes of token is sufficient.
- return true
- else
- # If there are scopes required, then check whether
- # the set of authorized scopes is a superset of the set of required scopes
- required_scopes = Set.new(scopes)
- authorized_scopes = Set.new(token.scopes)
-
- return authorized_scopes >= required_scopes
- end
- end
- end
-end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 15d7918e7fd..159f46cd465 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -95,7 +95,7 @@ module Projects
unless @project.gitlab_project_import?
@project.create_wiki unless skip_wiki?
- @project.build_missing_services
+ create_services_from_active_templates(@project)
@project.create_labels
end
@@ -106,6 +106,8 @@ module Projects
unless @project.group || @project.gitlab_project_import?
@project.team << [current_user, :master, current_user]
end
+
+ @project.group.refresh_members_authorized_projects if @project.group
end
def skip_wiki?
@@ -135,5 +137,12 @@ module Projects
@project
end
+
+ def create_services_from_active_templates(project)
+ Service.where(template: true, active: true).each do |template|
+ service = Service.build_from_template(project.id, template)
+ service.save!
+ end
+ end
end
end
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index d38328403c1..6040391fd94 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -1,7 +1,7 @@
module Projects
class ParticipantsService < BaseService
attr_reader :noteable
-
+
def execute(noteable)
@noteable = noteable
@@ -15,7 +15,8 @@ module Projects
[{
name: noteable.author.name,
- username: noteable.author.username
+ username: noteable.author.username,
+ avatar_url: noteable.author.avatar_url
}]
end
@@ -28,14 +29,14 @@ module Projects
def sorted(users)
users.uniq.to_a.compact.sort_by(&:username).map do |user|
- { username: user.username, name: user.name }
+ { username: user.username, name: user.name, avatar_url: user.avatar_url }
end
end
def groups
current_user.authorized_groups.sort_by(&:path).map do |group|
count = group.users.count
- { username: group.path, name: group.name, count: count }
+ { username: group.path, name: group.name, count: count, avatar_url: group.avatar.url }
end
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 28470f59807..34ec575e808 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -61,9 +61,6 @@ module Projects
# Move missing group labels to project
Labels::TransferService.new(current_user, old_group, project).execute
- # clear project cached events
- project.reset_events_cache
-
# Move uploads
Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path)
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 921ca6748d3..8a6af8d8ada 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -6,7 +6,7 @@ module Projects
if new_visibility && new_visibility.to_i != project.visibility_level
unless can?(current_user, :change_visibility_level, project) &&
- Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
+ Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(project, new_visibility)
return project
diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb
index 5a81194a5f4..d75c5b1800e 100644
--- a/app/services/slash_commands/interpret_service.rb
+++ b/app/services/slash_commands/interpret_service.rb
@@ -193,7 +193,7 @@ module SlashCommands
desc 'Subscribe'
condition do
issuable.persisted? &&
- !issuable.subscribed?(current_user)
+ !issuable.subscribed?(current_user, project)
end
command :subscribe do
@updates[:subscription_event] = 'subscribe'
@@ -202,7 +202,7 @@ module SlashCommands
desc 'Unsubscribe'
condition do
issuable.persisted? &&
- issuable.subscribed?(current_user)
+ issuable.subscribed?(current_user, project)
end
command :unsubscribe do
@updates[:subscription_event] = 'unsubscribe'
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 1ce66d50368..8b48d90f60b 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -21,7 +21,7 @@ module SystemNoteService
total_count = new_commits.length + existing_commits.length
commits_text = "#{total_count} commit".pluralize(total_count)
- body = "Added #{commits_text}:\n\n"
+ body = "added #{commits_text}\n\n"
body << existing_commit_summary(noteable, existing_commits, oldrev)
body << new_commit_summary(new_commits).join("\n")
body << "\n\n[Compare with previous version](#{diff_comparison_url(noteable, project, oldrev)})"
@@ -38,13 +38,13 @@ module SystemNoteService
#
# Example Note text:
#
- # "Assignee removed"
+ # "removed assignee"
#
- # "Reassigned to @rspeicher"
+ # "assigned to @rspeicher"
#
# Returns the created Note object
def change_assignee(noteable, project, author, assignee)
- body = assignee.nil? ? 'Assignee removed' : "Reassigned to #{assignee.to_reference}"
+ body = assignee.nil? ? 'removed assignee' : "assigned to #{assignee.to_reference}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -59,11 +59,11 @@ module SystemNoteService
#
# Example Note text:
#
- # "Added ~1 and removed ~2 ~3 labels"
+ # "added ~1 and removed ~2 ~3 labels"
#
- # "Added ~4 label"
+ # "added ~4 label"
#
- # "Removed ~5 label"
+ # "removed ~5 label"
#
# Returns the created Note object
def change_label(noteable, project, author, added_labels, removed_labels)
@@ -85,7 +85,6 @@ module SystemNoteService
end
body << ' ' << 'label'.pluralize(labels_count)
- body = body.capitalize
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -99,14 +98,13 @@ module SystemNoteService
#
# Example Note text:
#
- # "Milestone removed"
+ # "removed milestone"
#
- # "Miletone changed to 7.11"
+ # "changed milestone to 7.11"
#
# Returns the created Note object
def change_milestone(noteable, project, author, milestone)
- body = 'Milestone '
- body += milestone.nil? ? 'removed' : "changed to #{milestone.to_reference(project)}"
+ body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project)}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -121,50 +119,58 @@ module SystemNoteService
#
# Example Note text:
#
- # "Status changed to merged"
+ # "merged"
#
- # "Status changed to closed by bc17db76"
+ # "closed via bc17db76"
#
# Returns the created Note object
def change_status(noteable, project, author, status, source)
- body = "Status changed to #{status}"
- body << " by #{source.gfm_reference(project)}" if source
+ body = status.dup
+ body << " via #{source.gfm_reference(project)}" if source
create_note(noteable: noteable, project: project, author: author, note: body)
end
- # Called when 'merge when build succeeds' is executed
+ # Called when 'merge when pipeline succeeds' is executed
def merge_when_build_succeeds(noteable, project, author, last_commit)
- body = "Enabled an automatic merge when the build for #{last_commit.to_reference(project)} succeeds"
+ body = "enabled an automatic merge when the pipeline for #{last_commit.to_reference(project)} succeeds"
create_note(noteable: noteable, project: project, author: author, note: body)
end
- # Called when 'merge when build succeeds' is canceled
+ # Called when 'merge when pipeline succeeds' is canceled
def cancel_merge_when_build_succeeds(noteable, project, author)
- body = 'Canceled the automatic merge'
+ body = 'canceled the automatic merge'
create_note(noteable: noteable, project: project, author: author, note: body)
end
def remove_merge_request_wip(noteable, project, author)
- body = 'Unmarked this merge request as a Work In Progress'
+ body = 'unmarked as a Work In Progress'
create_note(noteable: noteable, project: project, author: author, note: body)
end
def add_merge_request_wip(noteable, project, author)
- body = 'Marked this merge request as a **Work In Progress**'
+ body = 'marked as a **Work In Progress**'
create_note(noteable: noteable, project: project, author: author, note: body)
end
def self.resolve_all_discussions(merge_request, project, author)
- body = "Resolved all discussions"
+ body = "resolved all discussions"
create_note(noteable: merge_request, project: project, author: author, note: body)
end
+ def discussion_continued_in_issue(discussion, project, author, issue)
+ body = "Added #{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)
+
+ create_note(note_attributes)
+ end
+
# Called when the title of a Noteable is changed
#
# noteable - Noteable object that responds to `title`
@@ -174,7 +180,7 @@ module SystemNoteService
#
# Example Note text:
#
- # "Title changed from **Old** to **New**"
+ # "changed title from **Old** to **New**"
#
# Returns the created Note object
def change_title(noteable, project, author, old_title)
@@ -185,7 +191,7 @@ module SystemNoteService
marked_old_title = Gitlab::Diff::InlineDiffMarker.new(old_title).mark(old_diffs, mode: :deletion, markdown: true)
marked_new_title = Gitlab::Diff::InlineDiffMarker.new(new_title).mark(new_diffs, mode: :addition, markdown: true)
- body = "Changed title: **#{marked_old_title}** → **#{marked_new_title}**"
+ body = "changed title from **#{marked_old_title}** to **#{marked_new_title}**"
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -197,11 +203,11 @@ 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)
- body = issue.confidential ? 'Made the issue confidential' : 'Made the issue visible'
+ body = issue.confidential ? 'made the issue confidential' : 'made the issue visible to everyone'
create_note(noteable: issue, project: project, author: author, note: body)
end
@@ -216,11 +222,11 @@ module SystemNoteService
#
# Example Note text:
#
- # "Target branch changed from `Old` to `New`"
+ # "changed target branch from `Old` to `New`"
#
# Returns the created Note object
def change_branch(noteable, project, author, branch_type, old_branch, new_branch)
- body = "#{branch_type} branch changed from `#{old_branch}` to `#{new_branch}`".capitalize
+ body = "changed #{branch_type} branch from `#{old_branch}` to `#{new_branch}`"
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -235,7 +241,7 @@ module SystemNoteService
#
# Example Note text:
#
- # "Restored target branch `feature`"
+ # "restored target branch `feature`"
#
# Returns the created Note object
def change_branch_presence(noteable, project, author, branch_type, branch, presence)
@@ -246,18 +252,18 @@ module SystemNoteService
'deleted'
end
- body = "#{verb} #{branch_type} branch `#{branch}`".capitalize
+ body = "#{verb} #{branch_type} branch `#{branch}`"
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when a branch is created from the 'new branch' button on a issue
# Example note text:
#
- # "Started branch `201-issue-branch-button`"
+ # "created branch `201-issue-branch-button`"
def new_issue_branch(issue, project, author, branch)
link = url_helpers.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
- body = "Started branch [`#{branch}`](#{link})"
+ body = "created branch [`#{branch}`](#{link})"
create_note(noteable: issue, project: project, author: author, note: body)
end
@@ -269,11 +275,11 @@ module SystemNoteService
#
# Example Note text:
#
- # "Mentioned in #1"
+ # "mentioned in #1"
#
- # "Mentioned in !2"
+ # "mentioned in !2"
#
- # "Mentioned in 54f7727c"
+ # "mentioned in 54f7727c"
#
# See cross_reference_note_content.
#
@@ -303,12 +309,12 @@ module SystemNoteService
end
def cross_reference?(note_text)
- note_text.start_with?(cross_reference_note_prefix)
+ note_text =~ /\A#{cross_reference_note_prefix}/i
end
# Check if a cross-reference is disallowed
#
- # This method prevents adding a "Mentioned in !1" note on every single commit
+ # This method prevents adding a "mentioned in !1" note on every single commit
# in a merge request. Additionally, it prevents the creation of references to
# external issues (which would fail).
#
@@ -370,12 +376,12 @@ module SystemNoteService
#
# Example Note text:
#
- # "Soandso marked the task Whatever as completed."
+ # "marked the task Whatever as completed."
#
# Returns the created Note object
def change_task_status(noteable, project, author, new_task)
status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE
- body = "Marked the task **#{new_task.source}** as #{status_label}"
+ body = "marked the task **#{new_task.source}** as #{status_label}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -388,7 +394,7 @@ module SystemNoteService
#
# Example Note text:
#
- # "Moved to some_namespace/project_new#11"
+ # "moved to some_namespace/project_new#11"
#
# Returns the created Note object
def noteable_moved(noteable, project, noteable_ref, author, direction:)
@@ -397,7 +403,7 @@ module SystemNoteService
end
cross_reference = noteable_ref.to_reference(project)
- body = "Moved #{direction} #{cross_reference}"
+ body = "moved #{direction} #{cross_reference}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -405,10 +411,12 @@ module SystemNoteService
def notes_for_mentioner(mentioner, noteable, notes)
if mentioner.is_a?(Commit)
- notes.where('note LIKE ?', "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}")
+ text = "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}"
+ notes.where('(note LIKE ? OR note LIKE ?)', text, text.capitalize)
else
gfm_reference = mentioner.gfm_reference(noteable.project)
- notes.where(note: cross_reference_note_content(gfm_reference))
+ text = cross_reference_note_content(gfm_reference)
+ notes.where(note: [text, text.capitalize])
end
end
@@ -417,7 +425,7 @@ module SystemNoteService
end
def cross_reference_note_prefix
- 'Mentioned in '
+ 'mentioned in '
end
def cross_reference_note_content(gfm_reference)
diff --git a/app/services/update_release_service.rb b/app/services/update_release_service.rb
index 0ee1ff2d7d9..b7c36651968 100644
--- a/app/services/update_release_service.rb
+++ b/app/services/update_release_service.rb
@@ -1,5 +1,3 @@
-require_relative 'base_service'
-
class UpdateReleaseService < BaseService
def execute(tag_name, release_description)
repository = project.repository
diff --git a/app/services/user_project_access_changed_service.rb b/app/services/user_project_access_changed_service.rb
new file mode 100644
index 00000000000..2469b4f0d7c
--- /dev/null
+++ b/app/services/user_project_access_changed_service.rb
@@ -0,0 +1,9 @@
+class UserProjectAccessChangedService
+ def initialize(user_ids)
+ @user_ids = Array.wrap(user_ids)
+ end
+
+ def execute
+ AuthorizedProjectsWorker.bulk_perform_async(@user_ids.map { |id| [id] })
+ end
+end
diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb
index b6c52ddac7a..86f317dcd18 100644
--- a/app/uploaders/artifact_uploader.rb
+++ b/app/uploaders/artifact_uploader.rb
@@ -1,4 +1,4 @@
-class ArtifactUploader < CarrierWave::Uploader::Base
+class ArtifactUploader < GitlabUploader
storage :file
attr_accessor :build, :field
@@ -38,12 +38,4 @@ class ArtifactUploader < CarrierWave::Uploader::Base
def exists?
file.try(:exists?)
end
-
- def move_to_cache
- true
- end
-
- def move_to_store
- true
- end
end
diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb
index fb3b5dfecd0..cfcb877cc3e 100644
--- a/app/uploaders/attachment_uploader.rb
+++ b/app/uploaders/attachment_uploader.rb
@@ -1,4 +1,4 @@
-class AttachmentUploader < CarrierWave::Uploader::Base
+class AttachmentUploader < GitlabUploader
include UploaderHelper
storage :file
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index 71ff14a3f20..a1ecb7bc00b 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -1,18 +1,12 @@
-class AvatarUploader < CarrierWave::Uploader::Base
+class AvatarUploader < GitlabUploader
include UploaderHelper
storage :file
- after :store, :reset_events_cache
-
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
- def reset_events_cache(file)
- model.reset_events_cache if model.is_a?(User)
- end
-
def exists?
model.avatar.file && model.avatar.file.exists?
end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 3ac6030c21c..47bef7cd1e4 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -1,4 +1,4 @@
-class FileUploader < CarrierWave::Uploader::Base
+class FileUploader < GitlabUploader
include UploaderHelper
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
new file mode 100644
index 00000000000..02d7c601d6c
--- /dev/null
+++ b/app/uploaders/gitlab_uploader.rb
@@ -0,0 +1,11 @@
+class GitlabUploader < CarrierWave::Uploader::Base
+ # Reduce disk IO
+ def move_to_cache
+ true
+ end
+
+ # Reduce disk IO
+ def move_to_store
+ true
+ end
+end
diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb
index 4f356dd663e..faab539b8e0 100644
--- a/app/uploaders/lfs_object_uploader.rb
+++ b/app/uploaders/lfs_object_uploader.rb
@@ -1,4 +1,4 @@
-class LfsObjectUploader < CarrierWave::Uploader::Base
+class LfsObjectUploader < GitlabUploader
storage :file
def store_dir
@@ -9,14 +9,6 @@ class LfsObjectUploader < CarrierWave::Uploader::Base
"#{Gitlab.config.lfs.storage_path}/tmp/cache"
end
- def move_to_cache
- true
- end
-
- def move_to_store
- true
- end
-
def exists?
file.try(:exists?)
end
diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb
index b10ad71d052..fbaea2744a3 100644
--- a/app/uploaders/uploader_helper.rb
+++ b/app/uploaders/uploader_helper.rb
@@ -1,6 +1,6 @@
# Extra methods for uploader
module UploaderHelper
- IMAGE_EXT = %w[png jpg jpeg gif bmp tiff]
+ IMAGE_EXT = %w[png jpg jpeg gif bmp tiff svg]
# We recommend using the .mp4 format over .mov. Videos in .mov format can
# still be used but you really need to make sure they are served with the
# proper MIME type video/mp4 and not video/quicktime or your videos won't play
diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb
index 2821ecf0a88..eb3ed31b65b 100644
--- a/app/validators/namespace_validator.rb
+++ b/app/validators/namespace_validator.rb
@@ -35,8 +35,22 @@ class NamespaceValidator < ActiveModel::EachValidator
users
].freeze
+ def self.valid?(value)
+ !reserved?(value) && follow_format?(value)
+ end
+
+ def self.reserved?(value)
+ RESERVED.include?(value)
+ end
+
+ def self.follow_format?(value)
+ value =~ Gitlab::Regex.namespace_regex
+ end
+
+ delegate :reserved?, :follow_format?, to: :class
+
def validate_each(record, attribute, value)
- unless value =~ Gitlab::Regex.namespace_regex
+ unless follow_format?(value)
record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
end
@@ -44,10 +58,4 @@ class NamespaceValidator < ActiveModel::EachValidator
record.errors.add(attribute, "#{value} is a reserved name")
end
end
-
- private
-
- def reserved?(value)
- RESERVED.include?(value)
- end
end
diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb
new file mode 100644
index 00000000000..927c67b65b0
--- /dev/null
+++ b/app/validators/project_path_validator.rb
@@ -0,0 +1,36 @@
+# ProjectPathValidator
+#
+# Custom validator for GitLab project path values.
+#
+# Values are checked for formatting and exclusion from a list of reserved path
+# names.
+class ProjectPathValidator < ActiveModel::EachValidator
+ # All project routes with wildcard argument must be listed here.
+ # Otherwise it can lead to routing issues when route considered as project name.
+ #
+ # Example:
+ # /group/project/tree/deploy_keys
+ #
+ # without tree as reserved name routing can match 'group/project' as group name,
+ # 'tree' as project name and 'deploy_keys' as route.
+ #
+ RESERVED = (NamespaceValidator::RESERVED +
+ %w[tree commits wikis new edit create update logs_tree
+ preview blob blame raw files create_dir find_file]).freeze
+
+ def self.valid?(value)
+ !reserved?(value)
+ end
+
+ def self.reserved?(value)
+ RESERVED.include?(value)
+ end
+
+ delegate :reserved?, to: :class
+
+ def validate_each(record, attribute, value)
+ if reserved?(value)
+ record.errors.add(attribute, "#{value} is a reserved name")
+ end
+ end
+end
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index 7bbc75db9ff..c4b748d0ab8 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -4,7 +4,7 @@
.abuse-reports
- if @abuse_reports.present?
.table-holder
- %table.table
+ %table.table.responsive-table
%thead.hidden-sm.hidden-xs
%tr
%th User
@@ -13,8 +13,6 @@
%th Action
= render @abuse_reports
- else
- .no-reports
- %span.pull-left
- There are no abuse reports!
- .pull-left
- = emoji_icon 'tada'
+ .empty-state
+ .text-center
+ %h4 There are no abuse reports! #{emoji_icon 'tada'}
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 450ec322f2c..7accd2529af 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -22,9 +22,8 @@
.form-group
= f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
.col-sm-10
- - data_attrs = { toggle: 'buttons' }
- .btn-group{ data: data_attrs }
- - restricted_level_checkboxes('restricted-visibility-help').each do |level|
+ - restricted_level_checkboxes('restricted-visibility-help').each do |level|
+ .checkbox
= level
%span.help-block#restricted-visibility-help
Selected levels cannot be used by non-admin users for projects or snippets.
@@ -32,10 +31,8 @@
.form-group
= f.label :import_sources, class: 'control-label col-sm-2'
.col-sm-10
- - data_attrs = { toggle: 'buttons' }
- .btn-group{ data: data_attrs }
- - import_sources_checkboxes('import-sources-help').each do |source|
- = source
+ - import_sources_checkboxes('import-sources-help').each do |source|
+ .checkbox= source
%span.help-block#import-sources-help
Enabled sources for code import during project creation. OmniAuth must be configured for GitHub
= link_to "(?)", help_page_path("integration/github")
@@ -284,6 +281,31 @@
results in fewer but larger UDP packets being sent.
%fieldset
+ %legend Background Jobs
+ %p
+ These settings require a restart to take effect.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :sidekiq_throttling_enabled do
+ = f.check_box :sidekiq_throttling_enabled
+ Enable Sidekiq Job Throttling
+ .help-block
+ Limit the amount of resources slow running jobs are assigned.
+ .form-group
+ = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
+ .help-block
+ Choose which queues you wish to throttle.
+ .form-group
+ = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
+ .help-block
+ The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
+
+ %fieldset
%legend Spam and Anti-bot Protection
.form-group
.col-sm-offset-2.col-sm-10
@@ -421,7 +443,16 @@
Some email servers do not support overriding the email sender name.
Enable this option to include the name of the author of the issue,
merge request or comment in the email body instead.
-
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :html_emails_enabled do
+ = f.check_box :html_emails_enabled
+ Enable HTML emails
+ .help-block
+ By default GitLab sends emails in HTML and plain text formats so mail
+ clients can choose what format to use. Disable this option if you only
+ want to send emails in plain text format.
%fieldset
%legend Automatic Git repository housekeeping
.form-group
diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml
index 4aacbb8cd77..c689b26d6e6 100644
--- a/app/views/admin/applications/_form.html.haml
+++ b/app/views/admin/applications/_form.html.haml
@@ -18,6 +18,12 @@
Use
%code= Doorkeeper.configuration.native_redirect_uri
for local tests
+
+ .form-group
+ = f.label :scopes, class: 'col-sm-2 control-label'
+ .col-sm-10
+ = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes
+
.form-actions
= f.submit 'Submit', class: "btn btn-save wide"
= link_to "Cancel", admin_applications_path, class: "btn btn-default"
diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml
index 3eb9d61972b..14683cc66e9 100644
--- a/app/views/admin/applications/show.html.haml
+++ b/app/views/admin/applications/show.html.haml
@@ -2,8 +2,7 @@
%h3.page-title
Application: #{@application.name}
-
-.table-holder
+.table-holder.oauth-application-show
%table.table
%tr
%td
@@ -23,6 +22,9 @@
- @application.redirect_uri.split.each do |uri|
%div
%span.monospace= uri
+
+ = render "shared/tokens/scopes_list", token: @application
+
.form-actions
= link_to 'Edit', edit_admin_application_path(@application), class: 'btn btn-primary wide pull-left'
= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10'
diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml
index 26a8846b609..5e3f105d41f 100644
--- a/app/views/admin/builds/index.html.haml
+++ b/app/views/admin/builds/index.html.haml
@@ -14,5 +14,5 @@
.row-content-block.second-block
#{(@scope || 'all').capitalize} builds
- %ul.content-list.builds-content-list
+ %ul.content-list.builds-content-list.admin-builds-table
= render "projects/builds/table", builds: @builds, admin: true
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index ec40391a3e3..b5f96363230 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -8,7 +8,7 @@
%span
Overview
= nav_link(controller: [:admin, :projects]) do
- = link_to admin_namespaces_projects_path, title: 'Projects' do
+ = link_to admin_projects_path, title: 'Projects' do
%span
Projects
= nav_link(controller: :users) do
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 1db2150f336..5238623e936 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -113,15 +113,15 @@
%hr
.row
.col-sm-4
- .light-well
+ .light-well.well-centered
%h4 Projects
.data
- = link_to admin_namespaces_projects_path do
+ = link_to admin_projects_path do
%h1= number_with_delimiter(Project.cached_count)
%hr
= link_to('New Project', new_project_path, class: "btn btn-new")
.col-sm-4
- .light-well
+ .light-well.well-centered
%h4 Users
.data
= link_to admin_users_path do
@@ -129,7 +129,7 @@
%hr
= link_to 'New User', new_admin_user_path, class: "btn btn-new"
.col-sm-4
- .light-well
+ .light-well.well-centered
%h4 Groups
.data
= link_to admin_groups_path do
@@ -143,7 +143,7 @@
%hr
- @projects.each do |project|
%p
- = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated'
+ = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60'
%span.light.pull-right
#{time_ago_with_tooltip(project.created_at)}
@@ -152,7 +152,7 @@
%hr
- @users.each do |user|
%p
- = link_to [:admin, user], class: 'str-truncated' do
+ = link_to [:admin, user], class: 'str-truncated-60' do
= user.name
%span.light.pull-right
#{time_ago_with_tooltip(user.created_at)}
@@ -162,7 +162,7 @@
%hr
- @groups.each do |group|
%p
- = link_to [:admin, group], class: 'str-truncated' do
+ = link_to [:admin, group], class: 'str-truncated-60' do
= group.name
%span.light.pull-right
#{time_ago_with_tooltip(group.created_at)}
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 817910f7ddf..589f4557b52 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -7,7 +7,7 @@
.col-sm-10
= render 'shared/choose_group_avatar_button', f: f
- = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
+ = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
.form-group
.col-sm-offset-2.col-sm-10
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index 664bb417c6a..cf28f92853e 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -2,7 +2,7 @@
%li.group-row{ class: css_class }
.controls
- = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
+ = link_to 'Edit', admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
= link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove'
.stats
%span
@@ -20,7 +20,7 @@
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
= link_to [:admin, group], class: 'group-name' do
- = group.name
+ = group.full_name
- if group.description.present?
.description
diff --git a/app/views/admin/groups/edit.html.haml b/app/views/admin/groups/edit.html.haml
index eb09a6328ed..c2b9807015d 100644
--- a/app/views/admin/groups/edit.html.haml
+++ b/app/views/admin/groups/edit.html.haml
@@ -1,4 +1,4 @@
- page_title "Edit", @group.name, "Groups"
%h3.page-title Edit group: #{@group.name}
%hr
-= render 'form'
+= render 'form', visibility_level: @group.visibility_level
diff --git a/app/views/admin/groups/new.html.haml b/app/views/admin/groups/new.html.haml
index c81ee552ac3..8f9fe96249f 100644
--- a/app/views/admin/groups/new.html.haml
+++ b/app/views/admin/groups/new.html.haml
@@ -1,4 +1,4 @@
- page_title "New Group"
%h3.page-title New group
%hr
-= render 'form'
+= render 'form', visibility_level: default_group_visibility
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 40871e32913..7b0175af214 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -1,8 +1,8 @@
- page_title @group.name, "Groups"
%h3.page-title
- Group: #{@group.name}
+ Group: #{@group.full_name}
- = link_to edit_admin_group_path(@group), class: "btn pull-right" do
+ = link_to admin_group_edit_path(@group), class: "btn pull-right" do
%i.fa.fa-pencil-square-o
Edit
%hr
@@ -88,7 +88,7 @@
Read more about project permissions
%strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
- = form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
+ = form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
%div
= users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all)
%div.prepend-top-10
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index 824edd171f3..0a954c20fcd 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -8,7 +8,7 @@
%div{ class: container_class }
%ul.nav-links.log-tabs
- loggers.each do |klass|
- %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }
+ %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }>
= link_to klass::file_name, "##{klass::file_name_noext}",
'data-toggle' => 'tab'
.row-content-block
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index b37b8d4fee7..8bc7dc7dd51 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -7,7 +7,7 @@
%div{ class: container_class }
.top-area
.prepend-top-default
- = form_tag admin_namespaces_projects_path, method: :get do |f|
+ = form_tag admin_projects_path, method: :get do |f|
.search-holder
.search-field-holder
= search_field_tag :name, params[:name], class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false, placeholder: 'Search by name'
@@ -41,19 +41,19 @@
= button_tag "Search", class: "btn btn-primary btn-search"
%ul.nav-links
- - opts = params[:visibility_level].present? ? {} : { page: admin_namespaces_projects_path }
+ - opts = params[:visibility_level].present? ? {} : { page: admin_projects_path }
= nav_link(opts) do
- = link_to admin_namespaces_projects_path do
+ = link_to admin_projects_path do
All
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s ? 'active' : '' }) do
- = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do
+ = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do
Private
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s ? 'active' : '' }) do
- = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do
+ = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do
Internal
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s ? 'active' : '' }) do
- = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
+ = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
Public
.nav-controls
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 73038164056..ca503e35623 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -91,7 +91,7 @@
%strong ##{build.id}
%td.status
- = ci_status_with_icon(build.status)
+ = render 'ci/status/badge', status: build.detailed_status(current_user)
%td.status
- if project
diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml
index cdbfc60f9a4..e5b8ebdf613 100644
--- a/app/views/admin/services/_form.html.haml
+++ b/app/views/admin/services/_form.html.haml
@@ -4,7 +4,8 @@
%p #{@service.description} template
= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'form-horizontal fieldset-form' } do |form|
- = render 'shared/service_settings', form: form
+ = render 'shared/service_settings', form: form, subject: @service
- .form-actions
- = form.submit 'Save', class: 'btn btn-save'
+ .footer-block.row-content-block
+ .form-actions
+ = form.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml
index 4bf1c9cde3c..2d9588f9d27 100644
--- a/app/views/admin/users/_user.html.haml
+++ b/app/views/admin/users/_user.html.haml
@@ -1,8 +1,8 @@
-%li.user-row
+%li.flex-row
.user-avatar
= image_tag avatar_icon(user), class: "avatar", alt: ''
- .user-details
- .user-name
+ .row-main-content
+ .user-name.row-title.str-truncated-100
= link_to user.name, [:admin, user]
- if user.blocked?
%span.label.label-danger blocked
@@ -12,7 +12,7 @@
%span.label.label-default External
- if user == current_user
%span It's you!
- .user-email
+ .row-second-line.str-truncated-100
= mail_to user.email, user.email
.controls
= link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn'
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index d3038ae644f..4dc44225d49 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -68,7 +68,7 @@
%small.badge= number_with_delimiter(User.without_projects.count)
.fade-right
- %ul.users-list.content-list
+ %ul.flex-list.content-list
- if @users.empty?
%li
.nothing-here-block No users found.
diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index 84b9ceb23b3..dd6b7303493 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -7,7 +7,7 @@
%ul.well-list
- @user.groups.each do |group|
%li
- %strong= group.name
+ %strong= link_to group.name, admin_group_path(group)
&ndash; access to
#{pluralize(group.projects.count, 'project')}
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index fbe3ab912b6..d8912eda314 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -1,7 +1,10 @@
- grouped_emojis = awardable.grouped_awards(with_thumbs: inline)
.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } }
- awards_sort(grouped_emojis).each do |emoji, awards|
- %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } }
+ %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button",
+ disabled: !current_user,
+ class: (award_active_class(awards, current_user)),
+ data: { placement: "bottom", title: award_user_list(awards, current_user) } }
= emoji_icon(emoji, sprite: false)
%span.award-control-text.js-counter
= awards.count
diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml
new file mode 100644
index 00000000000..f2135af2686
--- /dev/null
+++ b/app/views/ci/status/_badge.html.haml
@@ -0,0 +1,10 @@
+- status = local_assigns.fetch(:status)
+
+- if status.has_details?
+ = link_to status.details_path, class: "ci-status ci-#{status}" do
+ = custom_icon(status.icon)
+ = status.text
+- else
+ %span{ class: "ci-status ci-#{status}" }
+ = custom_icon(status.icon)
+ = status.text
diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml
new file mode 100644
index 00000000000..c7d04ab61e9
--- /dev/null
+++ b/app/views/ci/status/_graph_badge.html.haml
@@ -0,0 +1,19 @@
+-# Renders the graph node with both the status icon, status name and action icon
+
+- subject = local_assigns.fetch(:subject)
+- status = subject.detailed_status(current_user)
+- klass = "ci-status-icon ci-status-icon-#{status}"
+
+- if status.has_details?
+ = link_to status.details_path, data: { toggle: 'tooltip', title: "#{subject.name} - #{status.label}" } do
+ %span{ class: klass }= custom_icon(status.icon)
+ .ci-status-text= subject.name
+- else
+ %span{ class: klass }= custom_icon(status.icon)
+ .ci-status-text= subject.name
+
+- if status.has_action?
+ = link_to status.action_path, method: status.action_method,
+ title: status.action_title, class: 'ci-action-icon-container' do
+ %i.ci-action-icon-wrapper
+ = icon(status.action_icon, class: status.action_class)
diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml
index b78e70ebc1e..02b94beee92 100644
--- a/app/views/dashboard/_activity_head.html.haml
+++ b/app/views/dashboard/_activity_head.html.haml
@@ -1,7 +1,7 @@
%ul.nav-links
- %li{ class: ("active" unless params[:filter]) }
+ %li{ class: ("active" unless params[:filter]) }>
= link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects
- %li{ class: ("active" if params[:filter] == 'starred') }
+ %li{ class: ("active" if params[:filter] == 'starred') }>
= link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do
Starred Projects
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index f7abad54286..48b0fd504f4 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -4,13 +4,13 @@
%ul.nav-links
= nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
- Your Projects
+ Your projects
= nav_link(page: starred_dashboard_projects_path) do
= link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do
- Starred Projects
+ Starred projects
= nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do
= link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do
- Explore Projects
+ Explore projects
.nav-controls
= form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml
index b25e8ea1f0c..02e90bbfa55 100644
--- a/app/views/dashboard/_snippets_head.html.haml
+++ b/app/views/dashboard/_snippets_head.html.haml
@@ -1,7 +1,13 @@
-%ul.nav-links
- = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
- = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
- Your Snippets
- = nav_link(page: explore_snippets_path) do
- = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do
- Explore Snippets
+.top-area
+ %ul.nav-links
+ = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
+ = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
+ Your Snippets
+ = nav_link(page: explore_snippets_path) do
+ = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do
+ Explore Snippets
+
+ - if current_user
+ .nav-controls.hidden-xs
+ = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do
+ New snippet
diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index fdea834ff45..4a55aac0df6 100644
--- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
@@ -4,6 +4,18 @@
Welcome to GitLab
%p.blank-state-text
Code, test, and deploy together
+
+- if current_user.can_create_group?
+ .blank-state
+ .blank-state-icon
+ = custom_icon("group", size: 50)
+ %h3.blank-state-title
+ You can create a group for several dependent projects.
+ %p.blank-state-text
+ Groups are the best way to manage projects and members.
+ = link_to new_group_path, class: "btn btn-new" do
+ New group
+
.blank-state
.blank-state-icon
= custom_icon("project", size: 50)
@@ -21,17 +33,6 @@
= link_to new_project_path, class: "btn btn-new" do
New project
-- if current_user.can_create_group?
- .blank-state
- .blank-state-icon
- = custom_icon("group", size: 50)
- %h3.blank-state-title
- You can create a group for several dependent projects.
- %p.blank-state-text
- Groups are the best way to manage projects and members.
- = link_to new_group_path, class: "btn btn-new" do
- New group
-
-if publicish_project_count > 0
.blank-state
.blank-state-icon
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index b2af438ea57..85cbe0bf0e6 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -2,41 +2,11 @@
- header_title "Snippets", dashboard_snippets_path
= render 'dashboard/snippets_head'
+= render partial: 'snippets/snippets_scope_menu', locals: { include_private: true }
-.nav-block
- .controls.hidden-xs
- = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do
- = icon('plus')
- New snippet
+.visible-xs
+ &nbsp;
+ = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do
+ New snippet
- .nav-links.snippet-scope-menu
- %li{ class: ("active" unless params[:scope]) }
- = link_to dashboard_snippets_path do
- All
- %span.badge
- = current_user.snippets.count
-
- %li{ class: ("active" if params[:scope] == "are_private") }
- = link_to dashboard_snippets_path(scope: 'are_private') do
- Private
- %span.badge
- = current_user.snippets.are_private.count
-
- %li{ class: ("active" if params[:scope] == "are_internal") }
- = link_to dashboard_snippets_path(scope: 'are_internal') do
- Internal
- %span.badge
- = current_user.snippets.are_internal.count
-
- %li{ class: ("active" if params[:scope] == "are_public") }
- = link_to dashboard_snippets_path(scope: 'are_public') do
- Public
- %span.badge
- = current_user.snippets.are_public.count
-
- .visible-xs
- = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do
- = icon('plus')
- New snippet
-
-= render 'snippets/snippets'
+= render partial: 'snippets/snippets', locals: { link_project: true }
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 5b2465e25ee..e13f404fee2 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -5,14 +5,14 @@
.top-area
%ul.nav-links
- todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending')
- %li{class: "todos-pending #{todo_pending_active}"}
+ %li{class: "todos-pending #{todo_pending_active}"}>
= link_to todos_filter_path(state: 'pending') do
%span
To do
%span.badge
= number_with_delimiter(todos_pending_count)
- todo_done_active = ('active' if params[:state] == 'done')
- %li{class: "todos-done #{todo_done_active}"}
+ %li{class: "todos-done #{todo_done_active}"}>
= link_to todos_filter_path(state: 'done') do
%span
Done
@@ -32,7 +32,7 @@
- if params[:project_id].present?
= hidden_field_tag(:project_id, params[:project_id])
= dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
- placeholder: 'Search projects', data: { data: todo_projects_options } })
+ placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project' } })
.filter-item.inline
- if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id])
@@ -42,21 +42,21 @@
- if params[:type].present?
= hidden_field_tag(:type, params[:type])
= dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit',
- data: { data: todo_types_options } })
+ data: { data: todo_types_options, default_label: 'Type' } })
.filter-item.inline.actions-filter
- if params[:action_id].present?
= hidden_field_tag(:action_id, params[:action_id])
= dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
- data: { data: todo_actions_options }})
+ data: { data: todo_actions_options, default_label: 'Action' } })
.pull-right
.dropdown.inline.prepend-left-10
- %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'}
%span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- = icon('caret-down')
+ = icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
%li
= link_to todos_filter_path(sort: sort_value_priority) do
@@ -84,7 +84,7 @@
= render "shared/empty_states/todos_all_done.svg"
- if todos_filter_empty?
%h4.text-center
- Good job! Looks like you don't have any todos left.
+ = Gitlab.config.gitlab.no_todos_messages.sample
%p.text-center
Are you looking for things to do? Take a look at
= succeed "," do
diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml
index 20cd7b0179d..fb70d158096 100644
--- a/app/views/devise/confirmations/almost_there.haml
+++ b/app/views/devise/confirmations/almost_there.haml
@@ -1,12 +1,13 @@
-.well-confirmation.text-center
+.well-confirmation.text-center.append-bottom-20
%h1.prepend-top-0
Almost there...
- %p.lead
+ %p.lead.append-bottom-20
Please check your email to confirm your account
+ %hr
- if current_application_settings.after_sign_up_text.present?
.well-confirmation.text-center
= markdown_field(current_application_settings, :after_sign_up_text)
-%p.confirmation-content.text-center
+%p.text-center
No confirmation email received? Please check your spam folder or
.append-bottom-20.prepend-top-20.text-center
%a.btn.btn-lg.btn-success{ href: new_user_confirmation_path }
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 21b89580818..84e13693dfd 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -5,8 +5,6 @@
%div.form-group
= f.label :password
= f.password_field :password, class: "form-control bottom", required: true, title: "This field is required."
- %div.submit-container.move-submit-down
- = f.submit "Sign in", class: "btn btn-save"
- if devise_mapping.rememberable?
.remember-me.checkbox
%label{for: "user_remember_me"}
@@ -14,3 +12,5 @@
%span Remember me
.pull-right.forgot-password
= link_to "Forgot your password?", new_password_path(resource_name)
+ %div.submit-container.move-submit-down
+ = f.submit "Sign in", class: "btn btn-save"
diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml
index fa8e7979461..af87129e49e 100644
--- a/app/views/devise/sessions/new.html.haml
+++ b/app/views/devise/sessions/new.html.haml
@@ -1,4 +1,5 @@
- page_title "Sign in"
+
%div
- if form_based_providers.any?
= render 'devise/shared/tabs_ldap'
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 7c68e3266e5..3133f6de2e8 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -8,7 +8,7 @@
= f.text_field :name, class: "form-control top", required: true, title: "This field is required."
%div.username.form-group
= f.label :username
- = f.text_field :username, class: "form-control middle", pattern: "[a-zA-Z0-9]+", required: true, title: 'Please create a username with only alphanumeric characters.'
+ = f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, required: true, title: 'Please create a username with only alphanumeric characters.'
%p.validation-error.hide Username is already taken.
%p.validation-success.hide Username is available.
%p.validation-pending.hide Checking username availability...
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index 1e957f0935f..aec1b31ce62 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -8,3 +8,6 @@
- if signin_enabled?
%li
= link_to 'Standard', '#ldap-standard', 'data-toggle' => 'tab'
+ - if signin_enabled? && signup_enabled?
+ %li
+ = link_to 'Register', '#register-pane', 'data-toggle' => 'tab'
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index 077e8e64e5f..2bce2780484 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -1,9 +1,6 @@
- expanded = discussion.expanded?
%li.note.note-discussion.timeline-entry
.timeline-entry-inner
- .timeline-icon
- = link_to user_path(discussion.author) do
- = image_tag avatar_icon(discussion.author), class: "avatar s40"
.timeline-content
.discussion.js-toggle-container{ class: discussion.id, data: { discussion_id: discussion.id } }
.discussion-header
@@ -13,9 +10,7 @@
= icon("chevron-up")
- else
= icon("chevron-down")
-
Toggle discussion
-
= link_to_member(@project, discussion.author, avatar: false)
.inline.discussion-headline-light
@@ -37,7 +32,6 @@
an outdated diff
= time_ago_with_tooltip(discussion.created_at, placement: "bottom", html_class: "note-created-ago")
-
= render "discussions/headline", discussion: discussion
.discussion-body.js-toggle-content{ class: ("hide" unless expanded) }
diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml
index 5c98265727a..b3313c7c985 100644
--- a/app/views/doorkeeper/applications/_form.html.haml
+++ b/app/views/doorkeeper/applications/_form.html.haml
@@ -17,5 +17,9 @@
%code= Doorkeeper.configuration.native_redirect_uri
for local tests
+ .form-group
+ = f.label :scopes, class: 'label-light'
+ = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes
+
.prepend-top-default
= f.submit 'Save application', class: "btn btn-create"
diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml
index 47442b78d48..559de63d96d 100644
--- a/app/views/doorkeeper/applications/show.html.haml
+++ b/app/views/doorkeeper/applications/show.html.haml
@@ -2,7 +2,7 @@
%h3.page-title
Application: #{@application.name}
-.table-holder
+.table-holder.oauth-application-show
%table.table
%tr
%td
@@ -22,6 +22,9 @@
- @application.redirect_uri.split.each do |uri|
%div
%span.monospace= uri
+
+ = render "shared/tokens/scopes_list", token: @application
+
.form-actions
= link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide pull-left'
= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10'
diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml
index c034bbe430e..8bddbef3562 100644
--- a/app/views/errors/access_denied.html.haml
+++ b/app/views/errors/access_denied.html.haml
@@ -1,6 +1,10 @@
-- page_title "Access Denied"
-%h1 403
-%h3 Access Denied
-%hr
-%p You are not allowed to access this page.
-%p Read more about project permissions #{link_to "here", help_page_path("user/permissions"), class: "vlink"}
+- content_for(:title, 'Access Denied')
+%img{:alt => "GitLab Logo",
+ :src => image_path('logo.svg')}
+ %h1
+ 403
+.container
+ %h3 Access Denied
+ %hr
+ %p You are not allowed to access this page.
+ %p Read more about project permissions #{link_to "here", help_page_path("user/permissions"), class: "vlink"}
diff --git a/app/views/errors/encoding.html.haml b/app/views/errors/encoding.html.haml
index 90cfbebfcc6..064ff14ad2c 100644
--- a/app/views/errors/encoding.html.haml
+++ b/app/views/errors/encoding.html.haml
@@ -1,5 +1,9 @@
-- page_title "Encoding Error"
-%h1 500
-%h3 Encoding Error
-%hr
-%p Page can't be loaded because of an encoding error.
+- content_for(:title, 'Encoding Error')
+%img{:alt => "GitLab Logo",
+ :src => image_path('logo.svg')}
+ %h1
+ 500
+.container
+ %h3 Encoding Error
+ %hr
+ %p Page can't be loaded because of an encoding error.
diff --git a/app/views/errors/git_not_found.html.haml b/app/views/errors/git_not_found.html.haml
index ff5d4cc1506..c5c12a410ac 100644
--- a/app/views/errors/git_not_found.html.haml
+++ b/app/views/errors/git_not_found.html.haml
@@ -1,7 +1,10 @@
-- page_title "Git Resource Not Found"
-%h1 404
-%h3 Git Resource Not found
-%hr
-%p
- Application can't get access to some branch or commit in your repository. It
- may have been moved.
+- content_for(:title, 'Git Resource Not Found')
+%img{:alt => "GitLab Logo",
+ :src => image_path('logo.svg')}
+ %h1
+ 404
+.container
+ %h3 Git Resource Not found
+ %hr
+ %p Application can't get access to some branch or commit in your repository. It
+ may have been moved
diff --git a/app/views/errors/not_found.html.haml b/app/views/errors/not_found.html.haml
index 3756b98ebb2..50a54a93cb5 100644
--- a/app/views/errors/not_found.html.haml
+++ b/app/views/errors/not_found.html.haml
@@ -1,5 +1,9 @@
-- page_title "Not Found"
-%h1 404
-%h3 The resource you were looking for doesn't exist.
-%hr
-%p You may have mistyped the address or the page may have moved.
+- content_for(:title, 'Not Found')
+%img{:alt => "GitLab Logo",
+ :src => image_path('logo.svg')}
+ %h1
+ 404
+.container
+ %h3 The resource you were looking for doesn't exist.
+ %hr
+ %p You may have mistyped the address or the page may have moved.
diff --git a/app/views/errors/omniauth_error.html.haml b/app/views/errors/omniauth_error.html.haml
index 3e70e98a24c..d91f1878cb6 100644
--- a/app/views/errors/omniauth_error.html.haml
+++ b/app/views/errors/omniauth_error.html.haml
@@ -1,9 +1,13 @@
-- page_title "Auth Error"
-%h1 422
-%h3 Sign-in using #{@provider} auth failed
-%hr
-%p Sign-in failed because #{@error}.
-%p There are couple of steps you can take:
+- content_for(:title, 'Auth Error')
+%img{:alt => "GitLab Logo",
+ :src => image_path('logo.svg')}
+ %h1
+ 422
+.container
+ %h3 Sign-in using #{@provider} auth failed
+ %hr
+ %p Sign-in failed because #{@error}.
+ %p There are couple of steps you can take:
%ul
%li Try logging in using your email
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 31fdcc5e21b..a0bd14df209 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -1,16 +1,15 @@
- if event.visible_to_user?(current_user)
.event-item{ class: event_row_class(event) }
.event-item-timestamp
- #{time_ago_with_tooltip(event.created_at, skip_js: true)}
+ #{time_ago_with_tooltip(event.created_at)}
- = cache [event, current_application_settings, "v2.2"] do
- = author_avatar(event, size: 40)
+ = author_avatar(event, size: 40)
- - if event.created_project?
- = render "events/event/created_project", event: event
- - elsif event.push?
- = render "events/event/push", event: event
- - elsif event.commented?
- = render "events/event/note", event: event
- - else
- = render "events/event/common", event: event
+ - if event.created_project?
+ = render "events/event/created_project", event: event
+ - elsif event.push?
+ = render "events/event/push", event: event
+ - elsif event.commented?
+ = render "events/event/note", event: event
+ - else
+ = render "events/event/common", event: event
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 44fff49d99c..64ca3c32e01 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -18,7 +18,7 @@
- few_commits.each do |commit|
= render "events/commit", commit: commit, project: project, event: event
- - create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project)
+ - create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) && event.authored_by?(current_user)
- if event.commits_count > 1
%li.commits-stat
- if event.commits_count > 2
@@ -35,12 +35,12 @@
Compare #{from_label}...#{truncate_sha(event.commit_to)}
- if create_mr
- %span{"data-user-is" => event.author_id, "data-display" => "inline"}
+ %span
or
= link_to create_mr_path(project.default_branch, event.ref_name, project) do
create a merge request
- elsif create_mr
- %li.commits-stat{"data-user-is" => event.author_id}
+ %li.commits-stat
= link_to create_mr_path(project.default_branch, event.ref_name, project) do
Create Merge Request
- elsif event.rm_ref?
diff --git a/app/views/explore/_head.html.haml b/app/views/explore/_head.html.haml
index d8a57560788..a3b0709e261 100644
--- a/app/views/explore/_head.html.haml
+++ b/app/views/explore/_head.html.haml
@@ -1,5 +1,5 @@
-.explore-title
- %h3
+.explore-title.text-center
+ %h2
Explore GitLab
%p.lead
Discover projects, groups and snippets. Share your projects with others
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index a1b39d9e1a0..4e5d965ccbe 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -17,13 +17,13 @@
.pull-right
.dropdown.inline
- %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'}
%span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- = icon('caret-down')
+ = icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to explore_groups_path(sort: sort_value_recently_created) do
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index 4cff14b096b..5ea154c36b4 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -1,13 +1,13 @@
- if current_user
.dropdown
- %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
+ %button.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
= icon('globe')
%span.light Visibility:
- if params[:visibility_level].present?
= visibility_level_label(params[:visibility_level].to_i)
- else
Any
- = icon('caret-down')
+ = icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_projects_path(visibility_level: nil) do
@@ -20,14 +20,14 @@
- if @tags.present?
.dropdown
- %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
+ %button.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
= icon('tags')
%span.light Tags:
- if params[:tag].present?
= params[:tag]
- else
Any
- = icon('caret-down')
+ = icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_projects_path(tag: nil) do
diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml
index 7def9eacdc9..e5706d04736 100644
--- a/app/views/explore/snippets/index.html.haml
+++ b/app/views/explore/snippets/index.html.haml
@@ -6,12 +6,4 @@
- else
= render 'explore/head'
-.row-content-block
- - if current_user
- = link_to new_snippet_path, class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do
- New snippet
-
- .oneline
- Public snippets created by you and other users are listed here
-
-= render 'snippets/snippets'
+= render partial: 'snippets/snippets', locals: { link_project: true }
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index ebf9aca7700..bc5d3c797ac 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -21,6 +21,7 @@
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
%button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
= icon("search")
+ = render 'shared/members/sort_dropdown'
.panel.panel-default
.panel-heading
Users with access to
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index de8f53b6b52..9d05bff6c4e 100644
--- a/app/views/groups/group_members/update.js.haml
+++ b/app/views/groups/group_members/update.js.haml
@@ -1,3 +1,4 @@
:plain
var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}');
$("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
+ gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@group_member)}"));
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index dc6c1bb69de..b4aa4f24d9e 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -3,24 +3,27 @@
- if current_user
= auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@group.name} issues")
-.top-area
- = render 'shared/issuable/nav', type: :issues
- .nav-controls
+- if group_issues(@group).exists?
+ .top-area
+ = render 'shared/issuable/nav', type: :issues
- if current_user
- = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
- = icon('rss')
- %span.icon-label
- Subscribe
- = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
+ .nav-controls
+ = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
+ = icon('rss')
+ %span.icon-label
+ Subscribe
+ = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
-= render 'shared/issuable/filter', type: :issues
+ = render 'shared/issuable/filter', type: :issues
-.row-content-block.second-block
- Only issues from
- %strong #{@group.name}
- group are listed here.
- - if current_user
- To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
+ .row-content-block.second-block
+ Only issues from the
+ %strong #{@group.name}
+ group are listed here.
+ - if current_user
+ To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
-.prepend-top-default
- = render 'shared/issues'
+ .prepend-top-default
+ = 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 e6953d94531..dbbdb583a24 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -2,8 +2,9 @@
.top-area
= render 'shared/issuable/nav', type: :merge_requests
- .nav-controls
- = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
+ - if current_user
+ .nav-controls
+ = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
= render 'shared/issuable/filter', type: :merge_requests
diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml
index 23d438b2aa1..63cadfca530 100644
--- a/app/views/groups/milestones/new.html.haml
+++ b/app/views/groups/milestones/new.html.haml
@@ -34,21 +34,10 @@
= f.label :projects, "Projects", class: "control-label"
.col-sm-10
= f.collection_select :project_ids, @group.projects.non_archived, :id, :name,
- { selected: @group.projects.non_archived.pluck(:id) }, multiple: true, class: 'select2'
+ { selected: @group.projects.non_archived.pluck(:id) }, required: true, multiple: true, class: 'select2'
- .col-md-6
- .form-group
- = f.label :due_date, "Due Date", class: "control-label"
- .col-sm-10
- = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
+ = render "shared/milestones/form_dates", f: f
.form-actions
= f.submit 'Create Milestone', class: "btn-create btn"
= link_to "Cancel", group_milestones_path(@group), class: "btn btn-cancel"
-
-
-:javascript
- $(".datepicker").datepicker({
- dateFormat: "yy-mm-dd",
- onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) }
- }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val()));
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index b439b40a75a..52ce26a20b1 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -4,25 +4,23 @@
- if current_user
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
-.cover-block.groups-cover-block
+.group-home-panel.text-center
%div{ class: container_class }
.avatar-container.s70.group-avatar
= image_tag group_icon(@group), class: "avatar s70 avatar-tile"
- .group-info
- .cover-title
- %h1
- @#{@group.path}
- %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
- = visibility_level_icon(@group.visibility_level, fw: false)
+ %h1.group-title
+ @#{@group.path}
+ %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
+ = visibility_level_icon(@group.visibility_level, fw: false)
- .group-right-buttons.btn-group
- - if current_user
- .pull-left.append-right-10= render 'shared/members/access_request_buttons', source: @group
- = render 'shared/notifications/button', notification_setting: @notification_setting
+ - if @group.description.present?
+ .group-home-desc
+ = markdown_field(@group, :description)
- - if @group.description.present?
- .cover-desc.description
- = markdown_field(@group, :description)
+ - if current_user
+ .group-buttons
+ = render 'shared/members/access_request_buttons', source: @group
+ = render 'shared/notifications/button', notification_setting: @notification_setting
%div.groups-header{ class: container_class }
.top-area
diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml
index be257b51b9e..f6ebd76af9d 100644
--- a/app/views/help/show.html.haml
+++ b/app/views/help/show.html.haml
@@ -1,3 +1,3 @@
- page_title @path.split("/").reverse.map(&:humanize)
.documentation.wiki
- = markdown @markdown.gsub('$your_email', current_user.try(:email) || "email@example.com")
+ = markdown @markdown
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index f8b4b107513..ac09b71ae89 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -1,5 +1,6 @@
-- page_title "Bitbucket import"
-- header_title "Projects", root_path
+- page_title 'Bitbucket import'
+- header_title 'Projects', root_path
+
%h3.page-title
%i.fa.fa-bitbucket
Import projects from Bitbucket
@@ -10,13 +11,13 @@
%hr
%p
- if @incompatible_repos.any?
- = button_tag class: "btn btn-import btn-success js-import-all" do
+ = button_tag class: 'btn btn-import btn-success js-import-all' do
Import all compatible projects
- = icon("spinner spin", class: "loading-icon")
+ = icon('spinner spin', class: 'loading-icon')
- else
- = button_tag class: "btn btn-success js-import-all" do
+ = button_tag class: 'btn btn-import btn-success js-import-all' do
Import all projects
- = icon("spinner spin", class: "loading-icon")
+ = icon('spinner spin', class: 'loading-icon')
.table-responsive
%table.table.import-jobs
@@ -32,7 +33,7 @@
- @already_added_projects.each do |project|
%tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
%td
- = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: "_blank"
+ = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank'
%td
= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
@@ -47,31 +48,41 @@
= project.human_import_status_name
- @repos.each do |repo|
- %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"}
+ %tr{id: "repo_#{repo.owner}___#{repo.slug}"}
%td
- = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank"
+ = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: "_blank"
%td.import-target
- = import_project_target(repo['owner'], repo['slug'])
+ %fieldset.row
+ .input-group
+ .project-path.input-group-btn
+ - if current_user.can_select_namespace?
+ - selected = params[:namespace_id] || :current_user
+ - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner, path: repo.owner) } : {}
+ = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 }
+ - else
+ = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true
+ %span.input-group-addon /
+ = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
%td.import-actions.job-status
- = button_tag class: "btn btn-import js-add-to-import" do
+ = button_tag class: 'btn btn-import js-add-to-import' do
Import
- = icon("spinner spin", class: "loading-icon")
+ = icon('spinner spin', class: 'loading-icon')
- @incompatible_repos.each do |repo|
- %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"}
+ %tr{id: "repo_#{repo.owner}___#{repo.slug}"}
%td
- = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank"
+ = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank'
%td.import-target
%td.import-actions-job-status
- = label_tag "Incompatible Project", nil, class: "label label-danger"
+ = label_tag 'Incompatible Project', nil, class: 'label label-danger'
- if @incompatible_repos.any?
%p
One or more of your Bitbucket projects cannot be imported into GitLab
directly because they use Subversion or Mercurial for version control,
rather than Git. Please convert
- = link_to "them to Git,", "https://www.atlassian.com/git/tutorials/migrating-overview"
+ = 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, 'data-no-turbolink' => 'true'
again.
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } }
diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml
index 2fd4859c1c6..882fdf1317d 100644
--- a/app/views/invites/show.html.haml
+++ b/app/views/invites/show.html.haml
@@ -6,7 +6,7 @@
- if inviter = @member.created_by
by
= link_to inviter.name, user_url(inviter)
- to join
+ to join
- case @member.source
- when Project
- project = @member.source
@@ -20,11 +20,18 @@
= link_to group.name, group_url(group)
as #{@member.human_access}.
-- if @member.source.users.include?(current_user)
+- is_member = @member.source.users.include?(current_user)
+
+- if is_member
%p
However, you are already a member of this #{@member.source.is_a?(Group) ? "group" : "project"}.
Sign in using a different account to accept the invitation.
-- else
+
+- if @member.invite_email != current_user.email
+ %p
+ Note that this invitation was sent to #{mail_to @member.invite_email}, but you are signed in as #{link_to current_user.to_reference, user_url(current_user)} with email #{mail_to current_user.email}.
+
+- unless is_member
.actions
= link_to "Accept invitation", accept_invite_url(@token), method: :post, class: "btn btn-success"
= link_to "Decline", decline_invite_url(@token), method: :post, class: "btn btn-danger prepend-left-10"
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 757de92d6d4..3e488cf73b9 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -56,5 +56,3 @@
= render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id')
= render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id')
= render 'layouts/bootlint' if Rails.env.development?
-
- = render 'layouts/user_styles'
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index e138ebab018..3daa1e90a8c 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -3,6 +3,14 @@
- if project
:javascript
- GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: noteable_type, type_id: params[:id])}"
- GitLab.GfmAutoComplete.cachedData = undefined;
- GitLab.GfmAutoComplete.setup();
+ gl.GfmAutoComplete.dataSources = {
+ emojis: "#{emojis_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+ members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}",
+ issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+ mergeRequests: "#{merge_requests_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+ labels: "#{labels_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+ milestones: "#{milestones_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+ commands: "#{commands_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}"
+ };
+
+ gl.GfmAutoComplete.setup();
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 8aefdcb3d9b..54d02ee8e4b 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -22,9 +22,10 @@
= render "layouts/nav/#{nav}"
.content-wrapper{ class: "#{layout_nav_class}" }
= yield :sub_nav
- = render "layouts/broadcast"
- = render "layouts/flash"
- = yield :flash_message
+ .alert-wrapper
+ = render "layouts/broadcast"
+ = render "layouts/flash"
+ = yield :flash_message
%div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
- .content
+ .content{ id: "content-body" }
= yield
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index d7386105b7d..8e65bd12c56 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -13,7 +13,7 @@
.location-badge= label
.search-input-wrap
.dropdown{ data: { url: search_autocomplete_path } }
- = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }
+ = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }
.dropdown-menu.dropdown-select
= dropdown_content do
%ul
diff --git a/app/views/layouts/_user_styles.html.haml b/app/views/layouts/_user_styles.html.haml
deleted file mode 100644
index b76b3cb5510..00000000000
--- a/app/views/layouts/_user_styles.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-:css
- [data-user-is] {
- display: none !important;
- }
-
- [data-user-is="#{current_user.try(:id)}"] {
- display: block !important;
- }
-
- [data-user-is="#{current_user.try(:id)}"][data-display="inline"] {
- display: inline !important;
- }
-
- [data-user-is-not] {
- display: block !important;
- }
-
- [data-user-is-not][data-display="inline"] {
- display: inline !important;
- }
-
- [data-user-is-not="#{current_user.try(:id)}"] {
- display: none !important;
- }
diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml
index 7fbe065df00..a3b925f6afd 100644
--- a/app/views/layouts/errors.html.haml
+++ b/app/views/layouts/errors.html.haml
@@ -1,10 +1,59 @@
!!! 5
%html{ lang: "en"}
- = render "layouts/head"
- %body{class: "#{user_application_theme} application navless"}
- = Gon::Base.render_data
- = render "layouts/header/empty"
- .container.navless-container
- = render "layouts/flash"
- .error-page
- = yield
+ %head
+ %meta{:content => "width=device-width, initial-scale=1, maximum-scale=1", :name => "viewport"}
+ %title= yield(:title)
+ :css
+ body {
+ color: #666;
+ text-align: center;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ margin: auto;
+ font-size: 14px;
+ }
+
+ h1 {
+ font-size: 56px;
+ line-height: 100px;
+ font-weight: normal;
+ color: #456;
+ }
+
+ h2 {
+ font-size: 24px;
+ color: #666;
+ line-height: 1.5em;
+ }
+
+ h3 {
+ color: #456;
+ font-size: 20px;
+ font-weight: normal;
+ line-height: 28px;
+ }
+
+ hr {
+ max-width: 800px;
+ margin: 18px auto;
+ border: 0;
+ border-top: 1px solid #EEE;
+ border-bottom: 1px solid white;
+ }
+
+ img {
+ max-width: 40vw;
+ display: block;
+ margin: 40px auto;
+ }
+
+ .container {
+ margin: auto 20px;
+ }
+
+ ul {
+ margin: auto;
+ text-align: left;
+ display:inline-block;
+ }
+%body
+ = yield
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 7a9859262f7..5456be77aab 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,4 +1,5 @@
%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
+ %a{ href: "#content-body", tabindex: "1", class: "sr-only gl-accessibility" } Skip to content
%div{ class: "container-fluid" }
.header-content
%button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" }
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index a0356feef95..817e4bebb05 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,5 +1,4 @@
.nav-sidebar
- .sidebar-header Across GitLab
%ul.nav
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
@@ -26,12 +25,12 @@
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
%span
Issues
- %span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
+ %span.count= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened))
= nav_link(path: 'dashboard#merge_requests') do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
%span
Merge Requests
- %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
+ %span.count= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened))
= nav_link(controller: 'dashboard/snippets') do
= link_to dashboard_snippets_path, title: 'Snippets' do
%span
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index f7edb47b666..f3539fd372d 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -31,7 +31,7 @@
= link_to merge_requests_group_path(@group), title: 'Merge Requests' do
%span
Merge Requests
- - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute
+ - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
%span.badge.count= number_with_delimiter(merge_requests.count)
= nav_link(controller: [:group_members]) do
= link_to group_group_members_path(@group), title: 'Members' do
diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml
index 75275afc0f3..1579d8f1662 100644
--- a/app/views/layouts/nav/_group_settings.html.haml
+++ b/app/views/layouts/nav/_group_settings.html.haml
@@ -1,10 +1,8 @@
- if current_user
- can_admin_group = can?(current_user, :admin_group, @group)
- can_edit = can?(current_user, :admin_group, @group)
- - member = @group.members.find_by(user_id: current_user.id)
- - can_leave = member && can?(current_user, :destroy_group_member, member)
- - if can_admin_group || can_edit || can_leave
+ - if can_admin_group || can_edit
.controls
.dropdown.group-settings-dropdown
%a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'}
@@ -14,13 +12,7 @@
- if can_admin_group
= nav_link(path: 'groups#projects') do
= link_to 'Projects', projects_group_path(@group), title: 'Projects'
- - if can_edit || can_leave
+ - if can_edit && can_admin_group
%li.divider
- - if can_edit
%li
= link_to 'Edit Group', edit_group_path(@group)
- - if can_leave
- %li
- = link_to polymorphic_path([:leave, @group, :members]),
- data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do
- Leave Group
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index 6d514f669db..e06301bda14 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -17,6 +17,10 @@
= link_to applications_profile_path, title: 'Applications' do
%span
Applications
+ = nav_link(controller: :chat_names) do
+ = link_to profile_chat_names_path, title: 'Chat' do
+ %span
+ Chat
= nav_link(controller: :personal_access_tokens) do
= link_to profile_personal_access_tokens_path, title: 'Access Tokens' do
%span
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 99a58bbb676..904d11c2cf4 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -6,23 +6,14 @@
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- can_edit = can?(current_user, :admin_project, @project)
- -# We don't use @project.team.find_member because it searches for group members too...
- - member = @project.members.find_by(user_id: current_user.id)
- - can_leave = member && can?(current_user, :destroy_project_member, member)
= render 'layouts/nav/project_settings', can_edit: can_edit
- - if can_edit || can_leave
+ - if can_edit
%li.divider
- - if can_edit
- %li
- = link_to edit_project_path(@project) do
- Edit Project
- - if can_leave
- %li
- = link_to polymorphic_path([:leave, @project, :members]),
- data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do
- Leave Project
+ %li
+ = link_to edit_project_path(@project) do
+ Edit Project
.scrolling-tabs-container{ class: nav_control_class }
.fade-left
@@ -70,14 +61,14 @@
%span
Issues
- if @project.default_issues_tracker?
- %span.badge.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count)
+ %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
- if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do
= link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
%span
Merge Requests
- %span.badge.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count)
+ %span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
diff --git a/app/views/notify/_note_message.text.erb b/app/views/notify/_note_message.text.erb
new file mode 100644
index 00000000000..f82cbc9a3fc
--- /dev/null
+++ b/app/views/notify/_note_message.text.erb
@@ -0,0 +1,5 @@
+<% if current_application_settings.email_author_in_body %>
+ <%= @note.author_name %> wrote:
+<% end -%>
+
+<%= @note.note %>
diff --git a/app/views/notify/_note_mr_or_commit_email.html.haml b/app/views/notify/_note_mr_or_commit_email.html.haml
new file mode 100644
index 00000000000..edf8dfe7e9e
--- /dev/null
+++ b/app/views/notify/_note_mr_or_commit_email.html.haml
@@ -0,0 +1,18 @@
+= content_for :head do
+ = stylesheet_link_tag 'mailers/highlighted_diff_email'
+
+New comment
+
+- if @discussion && @discussion.diff_file
+ on
+ = link_to @note.diff_file.file_path, @target_url, class: 'details'
+ \:
+ %table
+ = render partial: "projects/diffs/line",
+ collection: @discussion.truncated_diff_lines,
+ as: :line,
+ locals: { diff_file: @note.diff_file,
+ plain: true,
+ email: true }
+
+= render 'note_message'
diff --git a/app/views/notify/_note_mr_or_commit_email.text.erb b/app/views/notify/_note_mr_or_commit_email.text.erb
new file mode 100644
index 00000000000..b4fcdf6b1e9
--- /dev/null
+++ b/app/views/notify/_note_mr_or_commit_email.text.erb
@@ -0,0 +1,8 @@
+<% if @discussion && @discussion.diff_file -%>
+ on <%= @note.diff_file.file_path -%>
+<% end -%>:
+
+<%= url %>
+
+<%= render 'simple_diff' if @discussion -%>
+<%= render 'note_message' %>
diff --git a/app/views/notify/_simple_diff.text.erb b/app/views/notify/_simple_diff.text.erb
new file mode 100644
index 00000000000..c28d1cc34d3
--- /dev/null
+++ b/app/views/notify/_simple_diff.text.erb
@@ -0,0 +1,3 @@
+<% @discussion.truncated_diff_lines(highlight: false).each do |line| %>
+> <%= line.text %>
+<% end %>
diff --git a/app/views/notify/links/ci/builds/_build.html.haml b/app/views/notify/links/ci/builds/_build.html.haml
new file mode 100644
index 00000000000..38cd4e5e145
--- /dev/null
+++ b/app/views/notify/links/ci/builds/_build.html.haml
@@ -0,0 +1,2 @@
+%a{href: pipeline_build_url(pipeline, build), style: "color:#3777b0;text-decoration:none;"}
+ = build.name
diff --git a/app/views/notify/links/ci/builds/_build.text.erb b/app/views/notify/links/ci/builds/_build.text.erb
new file mode 100644
index 00000000000..f495a2e5486
--- /dev/null
+++ b/app/views/notify/links/ci/builds/_build.text.erb
@@ -0,0 +1 @@
+Build #<%= build.id %> ( <%= pipeline_build_url(pipeline, build) %> )
diff --git a/app/views/notify/links/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.html.haml
new file mode 100644
index 00000000000..b6563b185b3
--- /dev/null
+++ b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.html.haml
@@ -0,0 +1 @@
+= build.name
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
new file mode 100644
index 00000000000..8e89c52a1f3
--- /dev/null
+++ b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb
@@ -0,0 +1 @@
+Build #<%= build.id %>
diff --git a/app/views/notify/note_commit_email.html.haml b/app/views/notify/note_commit_email.html.haml
index 1d961e4424c..0a650e3b2ca 100644
--- a/app/views/notify/note_commit_email.html.haml
+++ b/app/views/notify/note_commit_email.html.haml
@@ -1,2 +1,2 @@
-= render 'note_message'
-
+%p.details
+ = render 'note_mr_or_commit_email'
diff --git a/app/views/notify/note_commit_email.text.erb b/app/views/notify/note_commit_email.text.erb
index aaeaf5fdf73..6aa085a172e 100644
--- a/app/views/notify/note_commit_email.text.erb
+++ b/app/views/notify/note_commit_email.text.erb
@@ -1,9 +1,2 @@
-New comment for Commit <%= @commit.short_id %>
-
-<%= url_for(namespace_project_commit_url(@note.project.namespace, @note.project, id: @commit.id, anchor: "note_#{@note.id}")) %>
-
-
-Author: <%= @note.author_name %>
-
-<%= @note.note %>
-
+New comment for Commit <%= @commit.short_id -%>
+<%= render partial: 'note_mr_or_commit_email', locals: { url: @target_url } %>
diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml
index ea7e3d199fd..0a650e3b2ca 100644
--- a/app/views/notify/note_merge_request_email.html.haml
+++ b/app/views/notify/note_merge_request_email.html.haml
@@ -1,7 +1,2 @@
-- if @note.diff_note? && @note.diff_file
- %p.details
- New comment on diff for
- = link_to @note.diff_file.file_path, @target_url
- \:
-
-= render 'note_message'
+%p.details
+ = render 'note_mr_or_commit_email'
diff --git a/app/views/notify/note_merge_request_email.text.erb b/app/views/notify/note_merge_request_email.text.erb
index 8cdab63829e..2ce64c494cf 100644
--- a/app/views/notify/note_merge_request_email.text.erb
+++ b/app/views/notify/note_merge_request_email.text.erb
@@ -1,9 +1,2 @@
-New comment for Merge Request <%= @merge_request.to_reference %>
-
-<%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, anchor: "note_#{@note.id}")) %>
-
-
-<%= @note.author_name %>
-
-<%= @note.note %>
-
+New comment for Merge Request <%= @merge_request.to_reference -%>
+<%= render partial: 'note_mr_or_commit_email', locals: { url: @target_url }%>
diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml
index 38c852f0a3a..001d9c48555 100644
--- a/app/views/notify/pipeline_failed_email.html.haml
+++ b/app/views/notify/pipeline_failed_email.html.haml
@@ -158,12 +158,14 @@
%td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;"}
= build.stage
%td{align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"}
- %a{href: pipeline_build_url(@pipeline, build), style: "color:#3777b0;text-decoration:none;"}
- = build.name
+ = render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build
%tr.build-log
- %td{colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;"}
- %pre{style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;"}
- = build.trace_html(last_lines: 10).html_safe
+ - if build.has_trace?
+ %td{colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;"}
+ %pre{style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;"}
+ = build.trace_html(last_lines: 10).html_safe
+ - else
+ %td{colspan: "2"}
%tr.footer
%td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
%img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/
diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb
index 8f8084b58e1..ab91c7ef350 100644
--- a/app/views/notify/pipeline_failed_email.text.erb
+++ b/app/views/notify/pipeline_failed_email.text.erb
@@ -19,10 +19,12 @@ Commit Author: <%= commit.author_name %>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
<% failed.each do |build| -%>
-Build #<%= build.id %> ( <%= pipeline_build_url(@pipeline, build) %> )
+<%= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %>
Stage: <%= build.stage %>
Name: <%= build.name %>
+<% if build.has_trace? -%>
Trace: <%= build.trace_with_state(last_lines: 10)[:text] %>
+<% end -%>
<% end -%>
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
index 697c8d19257..56c1949ab2b 100644
--- a/app/views/notify/pipeline_success_email.html.haml
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -133,7 +133,7 @@
%tr.success-message
%td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;"}
- build_count = @pipeline.statuses.latest.size
- - stage_count = @pipeline.stages.size
+ - stage_count = @pipeline.stages_count
Pipeline
%a{href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;"}
= "\##{@pipeline.id}"
diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb
index ae22d474f2c..40e5e306426 100644
--- a/app/views/notify/pipeline_success_email.text.erb
+++ b/app/views/notify/pipeline_success_email.text.erb
@@ -16,7 +16,7 @@ Commit Author: <%= commit.author_name %>
<% end -%>
<% build_count = @pipeline.statuses.latest.size -%>
-<% stage_count = @pipeline.stages.size -%>
+<% stage_count = @pipeline.stages_count -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index c0c07d65daa..25883de257c 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -1,5 +1,5 @@
= content_for :head do
- = stylesheet_link_tag 'mailers/repository_push_email'
+ = stylesheet_link_tag 'mailers/highlighted_diff_email'
%h3
#{@message.author_name} #{@message.action_name} #{@message.ref_type} #{@message.ref_name}
@@ -27,9 +27,9 @@
%h4 #{pluralize @message.diffs_count, "changed file"}:
%ul
- - @message.diffs.each_with_index do |diff, i|
+ - @message.diffs.each do |diff|
%li.file-stats
- %a{href: "#{@message.target_url if @message.disable_diffs?}#diff-#{i}" }
+ %a{href: "#{@message.target_url if @message.disable_diffs?}##{hexdigest(diff.file_path)}" }
- if diff.deleted_file
%span.deleted-file
&minus;
@@ -52,9 +52,10 @@
%h5 The diff was not included because it is too large.
- else
%h4 Changes:
- - diff_files.each_with_index do |diff_file, i|
- %li{id: "diff-#{i}"}
- %a{href: @message.target_url + "#diff-#{i}"}<
+ - diff_files.each do |diff_file|
+ - file_hash = hexdigest(diff_file.file_path)
+ %li{id: file_hash}
+ %a{href: @message.target_url + "##{file_hash}"}<
- if diff_file.deleted_file
%strong<
= diff_file.old_path
diff --git a/app/views/profiles/chat_names/_chat_name.html.haml b/app/views/profiles/chat_names/_chat_name.html.haml
new file mode 100644
index 00000000000..1ec1e7c70e4
--- /dev/null
+++ b/app/views/profiles/chat_names/_chat_name.html.haml
@@ -0,0 +1,27 @@
+- service = chat_name.service
+- project = service.project
+%tr
+ %td
+ %strong
+ - if can?(current_user, :read_project, project)
+ = link_to project.name_with_namespace, project_path(project)
+ - else
+ .light N/A
+ %td
+ %strong
+ - if can?(current_user, :admin_project, project)
+ = link_to service.title, edit_namespace_project_service_path(project.namespace, project, service)
+ - else
+ = service.title
+ %td
+ = chat_name.team_domain
+ %td
+ = chat_name.chat_name
+ %td
+ - if chat_name.last_used_at
+ = time_ago_with_tooltip(chat_name.last_used_at)
+ - else
+ Never
+
+ %td
+ = link_to 'Remove', profile_chat_name_path(chat_name), method: :delete, class: 'btn btn-danger pull-right', data: { confirm: 'Are you sure you want to revoke this nickname?' }
diff --git a/app/views/profiles/chat_names/index.html.haml b/app/views/profiles/chat_names/index.html.haml
new file mode 100644
index 00000000000..20cc636b2da
--- /dev/null
+++ b/app/views/profiles/chat_names/index.html.haml
@@ -0,0 +1,30 @@
+- page_title 'Chat'
+= render 'profiles/head'
+
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ You can see your Chat accounts.
+
+ .col-lg-9
+ %h5 Active chat names (#{@chat_names.size})
+
+ - if @chat_names.present?
+ .table-responsive
+ %table.table.chat-names
+ %thead
+ %tr
+ %th Project
+ %th Service
+ %th Team domain
+ %th Nickname
+ %th Last used
+ %th
+ %tbody
+ = render @chat_names
+
+ - else
+ .settings-message.text-center
+ You don't have any active chat names.
diff --git a/app/views/profiles/chat_names/new.html.haml b/app/views/profiles/chat_names/new.html.haml
new file mode 100644
index 00000000000..f635acf96e2
--- /dev/null
+++ b/app/views/profiles/chat_names/new.html.haml
@@ -0,0 +1,15 @@
+%h3.page-title Authorization required
+%main{:role => "main"}
+ %p.h4
+ Authorize
+ %strong.text-info= @chat_name_params[:chat_name]
+ to use your account?
+
+ %hr
+ .actions
+ = form_tag profile_chat_names_path, method: :post do
+ = hidden_field_tag :token, @chat_name_token.token
+ = submit_tag "Authorize", class: "btn btn-success wide pull-left"
+ = form_tag deny_profile_chat_names_path, method: :delete do
+ = hidden_field_tag :token, @chat_name_token.token
+ = submit_tag "Deny", class: "btn btn-danger prepend-left-10"
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 844fce59704..d79a1a9f368 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -30,7 +30,7 @@
%br
.clearfix
.form-group.pull-left.global-notification-setting
- = render 'shared/notifications/button', notification_setting: @global_notification_setting, left_align: true
+ = render 'shared/notifications/button', notification_setting: @global_notification_setting
.clearfix
diff --git a/app/views/profiles/personal_access_tokens/_form.html.haml b/app/views/profiles/personal_access_tokens/_form.html.haml
new file mode 100644
index 00000000000..3f6efa33953
--- /dev/null
+++ b/app/views/profiles/personal_access_tokens/_form.html.haml
@@ -0,0 +1,21 @@
+- personal_access_token = local_assigns.fetch(:personal_access_token)
+- scopes = local_assigns.fetch(:scopes)
+
+= form_for [:profile, personal_access_token], method: :post, html: { class: 'js-requires-input' } do |f|
+
+ = form_errors(personal_access_token)
+
+ .form-group
+ = f.label :name, class: 'label-light'
+ = f.text_field :name, class: "form-control", required: true
+
+ .form-group
+ = f.label :expires_at, class: 'label-light'
+ = f.text_field :expires_at, class: "datepicker form-control"
+
+ .form-group
+ = f.label :scopes, class: 'label-light'
+ = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: personal_access_token, scopes: scopes
+
+ .prepend-top-default
+ = f.submit 'Create Personal Access Token', class: "btn btn-create"
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 05a2ea67aa2..bb4effeeeb1 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -28,21 +28,8 @@
Add a Personal Access Token
%p.profile-settings-content
Pick a name for the application, and we'll give you a unique token.
- = form_for [:profile, @personal_access_token],
- method: :post, html: { class: 'js-requires-input' } do |f|
- = form_errors(@personal_access_token)
-
- .form-group
- = f.label :name, class: 'label-light'
- = f.text_field :name, class: "form-control", required: true
-
- .form-group
- = f.label :expires_at, class: 'label-light'
- = f.text_field :expires_at, class: "datepicker form-control", required: false
-
- .prepend-top-default
- = f.submit 'Create Personal Access Token', class: "btn btn-create"
+ = render "form", personal_access_token: @personal_access_token, scopes: @scopes
%hr
@@ -56,6 +43,7 @@
%th Name
%th Created
%th Expires
+ %th Scopes
%th
%tbody
- @active_personal_access_tokens.each do |token|
@@ -67,6 +55,7 @@
= token.expires_at.to_date.to_s(:medium)
- else
%span.personal-access-tokens-never-expires-label Never
+ %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>"
%td= link_to "Revoke", revoke_profile_personal_access_token_path(token), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this token? This action cannot be undone." }
- else
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 03ac739ade5..558a1d56151 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -30,7 +30,7 @@
To add the entry manually, provide the following details to the application on your phone.
%p.prepend-top-0.append-bottom-0
Account:
- = current_user.email
+ = @account_string
%p.prepend-top-0.append-bottom-0
Key:
= current_user.otp_secret.scan(/.{4}/).join(' ')
diff --git a/app/views/profiles/update_username.js.haml b/app/views/profiles/update_username.js.haml
index de1337a2a24..5307e0b48cb 100644
--- a/app/views/profiles/update_username.js.haml
+++ b/app/views/profiles/update_username.js.haml
@@ -2,5 +2,6 @@
:plain
new Flash("Username successfully changed", "notice")
- else
+ - error = @user.errors.full_messages.first
:plain
- new Flash("Username change failed - #{@user.errors.full_messages.first}", "alert")
+ new Flash("Username change failed - #{escape_javascript error.html_safe}", "alert")
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index d011e51e696..4f15f2997fb 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -13,7 +13,7 @@
= spinner
:javascript
- var activity = new Activities();
+ var activity = new gl.Activities();
$(document).on('page:restore', function (event) {
activity.reloadActivities()
})
diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml
index 8e23d51b224..7f530708947 100644
--- a/app/views/projects/_last_commit.html.haml
+++ b/app/views/projects/_last_commit.html.haml
@@ -8,5 +8,5 @@
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message"
&middot;
-#{time_ago_with_tooltip(commit.committed_date, skip_js: true)} by
+#{time_ago_with_tooltip(commit.committed_date)} by
= commit_author_link(commit, avatar: true, size: 24)
diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml
new file mode 100644
index 00000000000..afe2fd7fd7b
--- /dev/null
+++ b/app/views/projects/_merge_request_merge_settings.html.haml
@@ -0,0 +1,15 @@
+- form = local_assigns.fetch(:form)
+
+.form-group
+ .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
+ %br
+ %span.descr
+ Builds need to be configured to enable this feature.
+ = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_build_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds')
+ .checkbox
+ = form.label :only_allow_merge_if_all_discussions_are_resolved do
+ = form.check_box :only_allow_merge_if_all_discussions_are_resolved
+ %strong Only allow merge requests to be merged if all discussions are resolved
diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml
index 6e143c4b570..818010bc7d3 100644
--- a/app/views/projects/_merge_request_settings.html.haml
+++ b/app/views/projects/_merge_request_settings.html.haml
@@ -1,18 +1,8 @@
-.merge-requests-feature
- %fieldset.builds-feature
- %hr
- %h5.prepend-top-0
- Merge Requests
- .form-group
- .checkbox
- = f.label :only_allow_merge_if_build_succeeds do
- = f.check_box :only_allow_merge_if_build_succeeds
- %strong Only allow merge requests to be merged if the build succeeds
- %br
- %span.descr
- Builds need to be configured to enable this feature.
- = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_build_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds')
- .checkbox
- = f.label :only_allow_merge_if_all_discussions_are_resolved do
- = f.check_box :only_allow_merge_if_all_discussions_are_resolved
- %strong Only allow merge requests to be merged if all discussions are resolved
+- form = local_assigns.fetch(:form)
+
+%fieldset.features.merge-requests-feature.append-bottom-default
+ %hr
+ %h5.prepend-top-0
+ Merge Requests
+
+ = render 'projects/merge_request_merge_settings', form: form
diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml
index 369a847e7d4..b6fb08b68e9 100644
--- a/app/views/projects/_readme.html.haml
+++ b/app/views/projects/_readme.html.haml
@@ -18,5 +18,5 @@
distributed with computer software, forming part of its documentation.
%p
We recommend you to
- = link_to "add a README", new_readme_path, class: 'underlined-link'
+ = link_to "add a README", add_special_file_path(@project, file_name: 'README.md'), class: 'underlined-link'
file to the repository and GitLab will render it here instead of this message.
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index 539d07d634a..ede01dcc1aa 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -1,5 +1,4 @@
- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds'
-- header_title project_title(@project, "Builds", project_builds_path(@project))
.top-block.row-content-block.clearfix
.pull-right
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index dfb96305f48..f63802ac88b 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -31,8 +31,8 @@
&nbsp;
.light
= commit_author_link(commit, avatar: false)
- authored
- #{time_ago_with_tooltip(commit.committed_date, skip_js: true)}
+ committed
+ #{time_ago_with_tooltip(commit.committed_date)}
%td.line-numbers
- line_count = blame_group[:lines].count
- (current_line...(current_line + line_count)).each do |i|
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 2a0352a71b7..a5dcd93f42e 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -27,5 +27,5 @@
= render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
= hidden_field_tag 'last_commit_sha', @last_commit_sha
= hidden_field_tag 'content', '', id: "file-content"
- = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id]
+ = hidden_field_tag 'from_merge_request_iid', params[:from_merge_request_iid]
= render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id)
diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml
new file mode 100644
index 00000000000..356bd50f7f3
--- /dev/null
+++ b/app/views/projects/boards/_show.html.haml
@@ -0,0 +1,28 @@
+- @no_container = true
+- @content_class = "issue-boards-content"
+- page_title "Boards"
+
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('boards/boards_bundle.js')
+ = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
+
+ %script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
+ %script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list"
+ %script#js-board-list-card{ type: "text/x-template" }= render "projects/boards/components/card"
+
+= render "projects/issues/head"
+
+= render 'shared/issuable/filter', type: :boards
+
+#board-app.boards-app{ "v-cloak" => true, data: board_data }
+ .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
+ .boards-app-loading.text-center{ "v-if" => "loading" }
+ = icon("spinner spin")
+ %board{ "v-cloak" => true,
+ "v-for" => "list in state.lists",
+ "ref" => "board",
+ ":list" => "list",
+ ":disabled" => "disabled",
+ ":issue-link-base" => "issueLinkBase",
+ ":key" => "_uid" }
+ = render "projects/boards/components/sidebar"
diff --git a/app/views/projects/boards/components/_blank_state.html.haml b/app/views/projects/boards/components/_blank_state.html.haml
index 97eb952eff1..0af40ddf8fe 100644
--- a/app/views/projects/boards/components/_blank_state.html.haml
+++ b/app/views/projects/boards/components/_blank_state.html.haml
@@ -1,5 +1,5 @@
%board-blank-state{ "inline-template" => true,
- "v-if" => "list.id == 'blank'" }
+ "v-if" => 'list.id == "blank"' }
.board-blank-state
%p
Add the following default lists to your Issue Board with one click:
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index f7071051efc..a2e5118a9f3 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -1,80 +1,34 @@
-%board{ "inline-template" => true,
- "v-cloak" => true,
- "v-for" => "list in state.lists | orderBy 'position'",
- "v-ref:board" => true,
- ":list" => "list",
- ":disabled" => "disabled",
- ":issue-link-base" => "issueLinkBase",
- "track-by" => "_uid" }
- .board{ ":class" => "{ 'is-draggable': !list.preset }",
- ":data-id" => "list.id" }
- .board-inner
- %header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
- %h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
- %span.has-tooltip{ ":title" => "(list.label ? list.label.description : '')",
- data: { container: "body", placement: "bottom" } }
- {{ list.title }}
- .board-issue-count-holder.pull-right.clearfix{ "v-if" => "list.type !== 'blank'" }
- %span.board-issue-count.pull-left{ ":class" => "{ 'has-btn': list.type !== 'done' }" }
- {{ list.issuesSize }}
- - if can?(current_user, :admin_issue, @project)
- %button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
- "@click" => "showNewIssueForm",
- "v-if" => "list.type !== 'done'",
- "aria-label" => "Add an issue",
- "title" => "Add an issue",
- data: { placement: "top", container: "body" } }
- = icon("plus")
- - if can?(current_user, :admin_list, @project)
- %board-delete{ "inline-template" => true,
- ":list" => "list",
- "v-if" => "!list.preset && list.id" }
- %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
- = icon("trash")
- %board-list{ "inline-template" => true,
- "v-if" => "list.type !== 'blank'",
- ":list" => "list",
- ":issues" => "list.issues",
- ":loading" => "list.loading",
- ":disabled" => "disabled",
- ":show-issue-form.sync" => "showIssueForm",
- ":issue-link-base" => "issueLinkBase" }
- .board-list-loading.text-center{ "v-if" => "loading" }
- = icon("spinner spin")
- - if can? current_user, :create_issue, @project
- %board-new-issue{ "inline-template" => true,
+.board{ ":class" => '{ "is-draggable": !list.preset }',
+ ":data-id" => "list.id" }
+ .board-inner
+ %header.board-header{ ":class" => '{ "has-border": list.label }', ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
+ %h3.board-title.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset) }' }
+ %span.has-tooltip{ ":title" => '(list.label ? list.label.description : "")',
+ data: { container: "body", placement: "bottom" } }
+ {{ list.title }}
+ .board-issue-count-holder.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' }
+ %span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "done" && !disabled }' }
+ {{ list.issuesSize }}
+ - if can?(current_user, :admin_issue, @project)
+ %button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
+ "@click" => "showNewIssueForm",
+ "v-if" => 'list.type !== "done"',
+ "aria-label" => "Add an issue",
+ "title" => "Add an issue",
+ data: { placement: "top", container: "body" } }
+ = icon("plus")
+ - if can?(current_user, :admin_list, @project)
+ %board-delete{ "inline-template" => true,
":list" => "list",
- ":show-issue-form.sync" => "showIssueForm",
- "v-show" => "list.type !== 'done' && showIssueForm" }
- .card.board-new-issue-form
- %form{ "@submit" => "submit($event)" }
- .flash-container{ "v-if" => "error" }
- .flash-alert
- An error occured. Please try again.
- %label.label-light{ ":for" => "list.id + '-title'" }
- Title
- %input.form-control{ type: "text",
- "v-model" => "title",
- "v-el:input" => true,
- ":id" => "list.id + '-title'" }
- .clearfix.prepend-top-10
- %button.btn.btn-success.pull-left{ type: "submit",
- ":disabled" => "title === ''",
- "v-el:submit-button" => true }
- Submit issue
- %button.btn.btn-default.pull-right{ type: "button",
- "@click" => "cancel" }
- Cancel
- %ul.board-list{ "v-el:list" => true,
- "v-show" => "!loading",
- ":data-board" => "list.id",
- ":class" => "{ 'is-smaller': showIssueForm }" }
- = render "projects/boards/components/card"
- %li.board-list-count.text-center{ "v-if" => "showCount" }
- = icon("spinner spin", "v-show" => "list.loadingMore" )
- %span{ "v-if" => "list.issues.length === list.issuesSize" }
- Showing all issues
- %span{ "v-else" => true }
- Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
- - if can?(current_user, :admin_list, @project)
- = render "projects/boards/components/blank_state"
+ "v-if" => "!list.preset && list.id" }
+ %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
+ = icon("trash")
+ %board-list{ "v-if" => 'list.type !== "blank"',
+ ":list" => "list",
+ ":issues" => "list.issues",
+ ":loading" => "list.loading",
+ ":disabled" => "disabled",
+ ":issue-link-base" => "issueLinkBase",
+ "ref" => "board-list" }
+ - if can?(current_user, :admin_list, @project)
+ = render "projects/boards/components/blank_state"
diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml
new file mode 100644
index 00000000000..34fdb1f6a74
--- /dev/null
+++ b/app/views/projects/boards/components/_board_list.html.haml
@@ -0,0 +1,44 @@
+.board-list-component
+ .board-list-loading.text-center{ "v-if" => "loading" }
+ = icon("spinner spin")
+ - if can? current_user, :create_issue, @project
+ %board-new-issue{ "inline-template" => true,
+ ":list" => "list",
+ "v-if" => 'list.type !== "done" && showIssueForm' }
+ .card.board-new-issue-form
+ %form{ "@submit" => "submit($event)" }
+ .flash-container{ "v-if" => "error" }
+ .flash-alert
+ An error occured. Please try again.
+ %label.label-light{ ":for" => 'list.id + "-title"' }
+ Title
+ %input.form-control{ type: "text",
+ "v-model" => "title",
+ "ref" => "input",
+ ":id" => 'list.id + "-title"' }
+ .clearfix.prepend-top-10
+ %button.btn.btn-success.pull-left{ type: "submit",
+ ":disabled" => 'title === ""',
+ "ref" => "submit-button" }
+ Submit issue
+ %button.btn.btn-default.pull-right{ type: "button",
+ "@click" => "cancel" }
+ Cancel
+ %ul.board-list{ "ref" => "list",
+ "v-show" => "!loading",
+ ":data-board" => "list.id",
+ ":class" => '{ "is-smaller": showIssueForm }' }
+ %board-card{ "v-for" => "(issue, index) in orderedIssues",
+ "ref" => "issue",
+ ":index" => "index",
+ ":list" => "list",
+ ":issue" => "issue",
+ ":issue-link-base" => "issueLinkBase",
+ ":disabled" => "disabled",
+ ":key" => "issue.id" }
+ %li.board-list-count.text-center{ "v-if" => "showCount" }
+ = icon("spinner spin", "v-show" => "list.loadingMore" )
+ %span{ "v-if" => "list.issues.length === list.issuesSize" }
+ Showing all issues
+ %span{ "v-else" => true }
+ Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
index 8fce702314c..1f31496e73f 100644
--- a/app/views/projects/boards/components/_card.html.haml
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -1,36 +1,28 @@
-%board-card{ "inline-template" => true,
- "v-for" => "issue in issues | orderBy 'priority'",
- "v-ref:issue" => true,
- ":index" => "$index",
- ":list" => "list",
- ":issue" => "issue",
- ":issue-link-base" => "issueLinkBase",
- ":disabled" => "disabled",
- "track-by" => "id" }
- %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }",
- ":index" => "index",
- "@mousedown" => "mouseDown",
- "@mouseMove" => "mouseMove",
- "@mouseup" => "showIssue($event)" }
- %h4.card-title
- = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
- %a{ ":href" => "issueLinkBase + '/' + issue.id",
- ":title" => "issue.title" }
- {{ issue.title }}
- .card-footer
- %span.card-number{ "v-if" => "issue.id" }
- = precede '#' do
- {{ issue.id }}
- %a.has-tooltip{ ":href" => "'#{root_path}' + issue.assignee.username",
- ":title" => "'Assigned to ' + issue.assignee.name",
- "v-if" => "issue.assignee",
- data: { container: 'body' } }
- %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 }
- %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
- type: "button",
- "v-if" => "(!list.label || label.id !== list.label.id)",
- "@click" => "filterByLabel(label, $event)",
- ":style" => "{ backgroundColor: label.color, color: label.textColor }",
- ":title" => "label.description",
- data: { container: 'body' } }
- {{ label.title }}
+%li.card{ ":class" => '{ "user-can-drag": !disabled && issue.id, "is-disabled": disabled || !issue.id, "is-active": issueDetailVisible }',
+ ":index" => "index",
+ ":data-issue-id" => "issue.id",
+ "@mousedown" => "mouseDown",
+ "@mousemove" => "mouseMove",
+ "@mouseup" => "showIssue($event)" }
+ %h4.card-title
+ = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
+ %a{ ":href" => 'issueLinkBase + "/" + issue.id',
+ ":title" => "issue.title" }
+ {{ issue.title }}
+ .card-footer
+ %span.card-number{ "v-if" => "issue.id" }
+ = precede '#' do
+ {{ issue.id }}
+ %a.has-tooltip{ ":href" => "\"#{root_path}\" + issue.assignee.username",
+ ":title" => '"Assigned to " + issue.assignee.name',
+ "v-if" => "issue.assignee",
+ data: { container: 'body' } }
+ %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 }
+ %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
+ type: "button",
+ "v-if" => "(!list.label || label.id !== list.label.id)",
+ "@click" => "filterByLabel(label, $event)",
+ ":style" => "{ backgroundColor: label.color, color: label.textColor }",
+ ":title" => "label.description",
+ data: { container: 'body' } }
+ {{ label.title }}
diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml
index f0c0c6953e0..2125c3387c4 100644
--- a/app/views/projects/boards/components/_sidebar.html.haml
+++ b/app/views/projects/boards/components/_sidebar.html.haml
@@ -1,5 +1,5 @@
%board-sidebar{ "inline-template" => true,
- ":current-user" => "#{current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) if current_user}" }
+ ":current-user" => "#{current_user ? current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) : {}}" }
%aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
.issuable-sidebar
.block.issuable-sidebar-header
diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/projects/boards/components/sidebar/_assignee.html.haml
index 604e13858d1..8fe1b832071 100644
--- a/app/views/projects/boards/components/sidebar/_assignee.html.haml
+++ b/app/views/projects/boards/components/sidebar/_assignee.html.haml
@@ -1,8 +1,8 @@
.block.assignee
.title.hide-collapsed
Assignee
- = icon("spinner spin", class: "block-loading")
- if can?(current_user, :admin_issue, @project)
+ = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "edit-link pull-right"
.value.hide-collapsed
%span.assign-yourself.no-value{ "v-if" => "!issue.assignee" }
diff --git a/app/views/projects/boards/components/sidebar/_due_date.html.haml b/app/views/projects/boards/components/sidebar/_due_date.html.haml
index c7da1d0d4ac..1a3b88e28c5 100644
--- a/app/views/projects/boards/components/sidebar/_due_date.html.haml
+++ b/app/views/projects/boards/components/sidebar/_due_date.html.haml
@@ -1,8 +1,8 @@
.block.due_date
.title
Due date
- = icon("spinner spin", class: "block-loading")
- if can?(current_user, :admin_issue, @project)
+ = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "edit-link pull-right"
.value
.value-content
diff --git a/app/views/projects/boards/components/sidebar/_labels.html.haml b/app/views/projects/boards/components/sidebar/_labels.html.haml
index ce68e5e1998..0f0a84c156d 100644
--- a/app/views/projects/boards/components/sidebar/_labels.html.haml
+++ b/app/views/projects/boards/components/sidebar/_labels.html.haml
@@ -1,8 +1,8 @@
.block.labels
.title
Labels
- = icon("spinner spin", class: "block-loading")
- if can?(current_user, :admin_issue, @project)
+ = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "edit-link pull-right"
.value.issuable-show-labels
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
diff --git a/app/views/projects/boards/components/sidebar/_milestone.html.haml b/app/views/projects/boards/components/sidebar/_milestone.html.haml
index 3cd20d1c0f7..008d1186478 100644
--- a/app/views/projects/boards/components/sidebar/_milestone.html.haml
+++ b/app/views/projects/boards/components/sidebar/_milestone.html.haml
@@ -1,8 +1,8 @@
.block.milestone
.title
Milestone
- = icon("spinner spin", class: "block-loading")
- if can?(current_user, :admin_issue, @project)
+ = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "edit-link pull-right"
.value
%span.no-value{ "v-if" => "!issue.milestone" }
diff --git a/app/views/projects/boards/components/sidebar/_notifications.html.haml b/app/views/projects/boards/components/sidebar/_notifications.html.haml
index 21c9563e9db..a08c7f2af09 100644
--- a/app/views/projects/boards/components/sidebar/_notifications.html.haml
+++ b/app/views/projects/boards/components/sidebar/_notifications.html.haml
@@ -1,11 +1,7 @@
- if current_user
.block.light.subscription{ ":data-url" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '/toggle_subscription'" }
- .title
+ %span.issuable-header-text.hide-collapsed.pull-left
Notifications
- %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
- {{ issue.subscribed ? 'Unsubscribe' : 'Subscribe' }}
- .subscription-status{ ":data-status" => "issue.subscribed ? 'subscribed' : 'unsubscribed'" }
- .unsubscribed{ "v-show" => "!issue.subscribed" }
- You're not receiving notifications from this thread.
- .subscribed{ "v-show" => "issue.subscribed" }
- You're receiving notifications because you're subscribed to this thread.
+ %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
+ %span
+ {{issue.subscribed ? 'Unsubscribe' : 'Subscribe'}}
diff --git a/app/views/projects/boards/index.html.haml b/app/views/projects/boards/index.html.haml
index 29c9a43a0c1..2a5b8b1441e 100644
--- a/app/views/projects/boards/index.html.haml
+++ b/app/views/projects/boards/index.html.haml
@@ -1,18 +1 @@
-- @no_container = true
-- @content_class = "issue-boards-content"
-- page_title "Boards"
-
-- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('boards/boards_bundle.js')
- = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
-
-= render "projects/issues/head"
-
-= render 'shared/issuable/filter', type: :boards
-
-#board-app.boards-app{ "v-cloak" => true, data: board_data }
- .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
- .boards-app-loading.text-center{ "v-if" => "loading" }
- = icon("spinner spin")
- = render "projects/boards/components/board"
- = render "projects/boards/components/sidebar"
+= render "show"
diff --git a/app/views/projects/boards/show.html.haml b/app/views/projects/boards/show.html.haml
index 29c9a43a0c1..2a5b8b1441e 100644
--- a/app/views/projects/boards/show.html.haml
+++ b/app/views/projects/boards/show.html.haml
@@ -1,18 +1 @@
-- @no_container = true
-- @content_class = "issue-boards-content"
-- page_title "Boards"
-
-- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('boards/boards_bundle.js')
- = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
-
-= render "projects/issues/head"
-
-= render 'shared/issuable/filter', type: :boards
-
-#board-app.boards-app{ "v-cloak" => true, data: board_data }
- .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
- .boards-app-loading.text-center{ "v-if" => "loading" }
- = icon("spinner spin")
- = render "projects/boards/components/board"
- = render "projects/boards/components/sidebar"
+= render "show"
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 84f38575e84..5fd664c7a93 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -12,10 +12,10 @@
= search_field_tag :search, params[:search], { placeholder: 'Filter by branch name', id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false }
.dropdown.inline
- %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'}
%span.light
= projects_sort_options_hash[@sort]
- = icon('caret-down')
+ = icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_branches_path(sort: sort_value_name) do
@@ -26,6 +26,8 @@
= sort_title_oldest_updated
- if can? current_user, :push_code, @project
+ = link_to namespace_project_merged_branches_path(@project.namespace, @project), class: 'btn btn-inverted btn-remove has-tooltip', title: "Delete all branches that are merged into '#{@project.repository.root_ref}'", method: :delete, data: { confirm: "Deleting the merged branches cannot be undone. Are you sure?", container: 'body' } do
+ Delete merged branches
= link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do
New branch
diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml
index 3f2ce7377fd..057a720a54a 100644
--- a/app/views/projects/builds/_header.html.haml
+++ b/app/views/projects/builds/_header.html.haml
@@ -1,8 +1,11 @@
.content-block.build-header
.header-content
- = ci_status_with_icon(@build.status)
+ = render 'ci/status/badge', status: @build.detailed_status(current_user)
Build
%strong ##{@build.id}
+ in pipeline
+ = link_to pipeline_path(@build.pipeline) do
+ %strong ##{@build.pipeline.id}
for commit
= link_to ci_status_path(@build.pipeline) do
%strong= @build.pipeline.short_sha
@@ -14,6 +17,6 @@
= render "user"
= time_ago_with_tooltip(@build.created_at)
- if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted pull-right', method: :post
+ = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post
%button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index 28f519f11b2..ce8b66b1945 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -111,16 +111,16 @@
%span.label.label-primary
= tag
- - if @build.pipeline.stages.many?
+ - if @build.pipeline.stages_count > 1
.dropdown.build-dropdown
.title Stage
%button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
%span.stage-selection More
- = icon('caret-down')
+ = icon('chevron-down')
%ul.dropdown-menu
- @build.pipeline.stages.each do |stage|
%li
- %a.stage-item= stage
+ %a.stage-item= stage.name
.builds-container
- HasStatus::ORDERED_STATUSES.each do |build_status|
@@ -128,7 +128,8 @@
.build-job{class: sidebar_build_class(build, @build), data: {stage: build.stage}}
= link_to namespace_project_build_path(@project.namespace, @project, build) do
= icon('arrow-right')
- = ci_icon_for_status(build.status)
+ %span{class: "ci-status-icon-#{build.status}"}
+ = ci_icon_for_status(build.status)
%span
- if build.name
= build.name
diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml
index 36294c89fa8..028664f5bba 100644
--- a/app/views/projects/builds/_table.html.haml
+++ b/app/views/projects/builds/_table.html.haml
@@ -10,6 +10,7 @@
%tr
%th Status
%th Build
+ %th Pipeline
- if admin
%th Project
%th Runner
@@ -19,6 +20,6 @@
%th Coverage
%th
- = render partial: "projects/ci/builds/build", collection: builds, as: :build, locals: { commit_sha: true, ref: true, stage: true, allow_retry: true, coverage: admin || project.build_coverage_enabled?, admin: admin }
+ = render partial: "projects/ci/builds/build", collection: builds, as: :build, locals: { commit_sha: true, ref: true, pipeline_link: true, stage: true, allow_retry: true, coverage: admin || project.build_coverage_enabled?, admin: admin }
= paginate builds, theme: 'gitlab'
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index b5e8b0bf6eb..cdeb81372ee 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -1,7 +1,6 @@
- @no_container = true
- page_title "#{@build.name} (##{@build.id})", "Builds"
- trace_with_state = @build.trace_with_state
-- header_title project_title(@project, "Builds", project_builds_path(@project))
= render "projects/pipelines/head", build_subnav: true
%div{ class: container_class }
@@ -27,33 +26,50 @@
= link_to namespace_project_runners_path(@build.project.namespace, @build.project) do
Runners page
+ - if @build.starts_environment?
+ .prepend-top-default
+ .environment-information
+ - if @build.outdated_deployment?
+ = ci_icon_for_status('success_with_warnings')
+ - else
+ = ci_icon_for_status(@build.status)
+
+ - environment = environment_for_build(@build.project, @build)
+ - if @build.success? && @build.last_deployment.present?
+ - if @build.last_deployment.last?
+ This build is the most recent deployment to #{environment_link_for_build(@build.project, @build)}.
+ - else
+ This build is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}.
+ View the most recent deployment #{deployment_link(environment.last_deployment)}.
+ - elsif @build.complete? && !@build.success?
+ The deployment of this build to #{environment_link_for_build(@build.project, @build)} did not succeed.
+ - else
+ This build is creating a deployment to #{environment_link_for_build(@build.project, @build)}
+ - if environment.try(:last_deployment)
+ and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')}
+
.prepend-top-default
- - if @build.active?
- .autoscroll-container
- %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
- if @build.erased?
.erased.alert.alert-warning
- erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by
Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)}
- else
#js-build-scroll.scroll-controls
- = link_to '#build-trace', class: 'btn' do
- %i.fa.fa-angle-up
- = link_to '#down-build-trace', class: 'btn' do
- %i.fa.fa-angle-down
+ .scroll-step
+ = link_to '#build-trace', class: 'btn' do
+ %i.fa.fa-angle-up
+ = link_to '#down-build-trace', class: 'btn' do
+ %i.fa.fa-angle-down
+ - if @build.active?
+ .autoscroll-container
+ %button.btn.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}}
+ Enable autoscroll
%pre.build-trace#build-trace
%code.bash.js-build-output
= icon("refresh spin", class: "js-build-refresh")
- #down-build-trace
+ #down-build-trace
= render "sidebar"
- :javascript
- new Build({
- page_url: "#{namespace_project_build_url(@project.namespace, @project, @build)}",
- build_url: "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}",
- build_status: "#{@build.status}",
- build_stage: "#{@build.stage}",
- state1: "#{trace_with_state[:state]}"
- })
+.js-build-options{ data: javascript_build_options }
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 7e83a88913a..40bfa01a45a 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,42 +1,41 @@
- if !project.empty_repo? && can?(current_user, :download_code, project)
- %span{class: 'hidden-xs hidden-sm download-button'}
- .dropdown.inline
- %button.btn{ 'data-toggle' => 'dropdown' }
- = icon('download')
- = icon("caret-down")
- %span.sr-only
- Select Archive Format
- %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
- %li.dropdown-header Source code
- %li
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
- %i.fa.fa-download
- %span Download zip
- %li
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
- %i.fa.fa-download
- %span Download tar.gz
- %li
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do
- %i.fa.fa-download
- %span Download tar.bz2
- %li
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do
- %i.fa.fa-download
- %span Download tar
+ .dropdown.inline.download-button
+ %button.btn{ 'data-toggle' => 'dropdown' }
+ = icon('download')
+ = icon("caret-down")
+ %span.sr-only
+ Select Archive Format
+ %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
+ %li.dropdown-header Source code
+ %li
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download zip
+ %li
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download tar.gz
+ %li
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download tar.bz2
+ %li
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download tar
- - pipeline = project.pipelines.latest_successful_for(ref)
- - if pipeline
- - artifacts = pipeline.builds.latest.with_artifacts
- - if artifacts.any?
- %li.dropdown-header Artifacts
- - unless pipeline.latest?
- - latest_pipeline = project.pipeline_for(ref)
- %li
- .unclickable= ci_status_for_statuseable(latest_pipeline)
- %li.dropdown-header Previous Artifacts
- - artifacts.each do |job|
- %li
- = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow' do
- %i.fa.fa-download
- %span Download '#{job.name}'
+ - pipeline = project.pipelines.latest_successful_for(ref)
+ - if pipeline
+ - artifacts = pipeline.builds.latest.with_artifacts
+ - if artifacts.any?
+ %li.dropdown-header Artifacts
+ - unless pipeline.latest?
+ - latest_pipeline = project.pipeline_for(ref)
+ %li
+ .unclickable= ci_status_for_statuseable(latest_pipeline)
+ %li.dropdown-header Previous Artifacts
+ - artifacts.each do |job|
+ %li
+ = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download '#{job.name}'
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index 6cd9b98a706..d3ccebbe290 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -1,5 +1,5 @@
- if current_user
- .dropdown.inline.project-dropdown
+ .dropdown.inline
%a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
= icon('plus')
= icon("caret-down")
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 94632056b15..f1cb0201032 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -2,16 +2,14 @@
- ref = local_assigns.fetch(:ref, nil)
- commit_sha = local_assigns.fetch(:commit_sha, nil)
- retried = local_assigns.fetch(:retried, false)
+- pipeline_link = local_assigns.fetch(:pipeline_link, false)
- stage = local_assigns.fetch(:stage, false)
- coverage = local_assigns.fetch(:coverage, false)
- allow_retry = local_assigns.fetch(:allow_retry, false)
%tr.build.commit{class: ('retried' if retried)}
%td.status
- - if can?(current_user, :read_build, build)
- = ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build))
- - else
- = ci_status_with_icon(build.status)
+ = render "ci/status/badge", status: build.detailed_status(current_user)
%td.branch-commit
- if can?(current_user, :read_build, build)
@@ -51,12 +49,20 @@
- if build.manual?
%span.label.label-info manual
+ - if pipeline_link
+ %td
+ = link_to pipeline_path(build.pipeline) do
+ %span.pipeline-id ##{build.pipeline.id}
+ %span by
+ - if build.pipeline.user
+ = user_avatar(user: build.pipeline.user, size: 20)
+ - else
+ %span.monospace API
+
- if admin
%td
- if build.project
= link_to build.project.name_with_namespace, admin_namespace_project_path(build.project.namespace, build.project)
-
- - if admin
%td
- if build.try(:runner)
= runner_link(build.runner)
@@ -82,9 +88,8 @@
%span #{time_ago_with_tooltip(build.finished_at)}
%td.coverage
- - if coverage
- - if build.try(:coverage)
- #{build.coverage}%
+ - if coverage && build.try(:coverage)
+ #{build.coverage}%
%td
.pull-right
@@ -96,9 +101,9 @@
= link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do
= icon('remove', class: 'cred')
- elsif allow_retry
- - if build.retryable?
- = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do
- = icon('repeat')
- - elsif build.playable? && !admin
+ - if build.playable? && !admin
= link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do
= custom_icon('icon_play')
+ - elsif build.retryable?
+ = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do
+ = icon('repeat')
diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml
deleted file mode 100644
index 93dca81e6f9..00000000000
--- a/app/views/projects/ci/builds/_build_pipeline.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-- is_playable = subject.playable? && can?(current_user, :update_build, @project)
-- if is_playable
- = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.pipeline-graph', placement: 'bottom' } do
- = ci_icon_for_status('play')
- .ci-status-text= subject.name
-- elsif can?(current_user, :read_build, @project)
- = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do
- %span.ci-status-icon
- = ci_icon_for_status(subject.status)
- .ci-status-text= subject.name
-- else
- %span.ci-status-icon
- = ci_icon_for_status(subject.status)
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index 1f748d73d06..3f05a21990f 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -4,8 +4,7 @@
%tr.commit
%td.commit-link
- = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
- = ci_status_with_icon(status)
+ = render 'ci/status/badge', status: pipeline.detailed_status(current_user)
%td
= link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
@@ -41,15 +40,13 @@
- else
Cant find HEAD commit for this branch
- - stages_status = pipeline.statuses.latest.stages_status
%td.stage-cell
- - stages.each do |stage|
- - status = stages_status[stage]
- - tooltip = "#{stage.titleize}: #{status || 'not found'}"
- - if status
+ - pipeline.stages.each do |stage|
+ - if stage.status
+ - tooltip = "#{stage.name.titleize}: #{stage.status || 'not found'}"
.stage-container
- = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do
- = ci_icon_for_status(status)
+ = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage.name), class: "has-tooltip ci-status-icon-#{stage.status}", title: tooltip do
+ = ci_icon_for_status(stage.status)
%td
- if pipeline.duration
@@ -59,7 +56,7 @@
- if pipeline.finished_at
%p.finished-at
= icon("calendar")
- #{time_ago_with_tooltip(pipeline.finished_at, short_format: false, skip_js: true)}
+ #{time_ago_with_tooltip(pipeline.finished_at, short_format: false)}
%td.pipeline-actions.hidden-xs
.controls.pull-right
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index e4cd55b9f7a..782f558e8b0 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -11,9 +11,9 @@
.modal-content
.modal-header
%a.close{href: "#", "data-dismiss" => "modal"} ×
- %h3.page-title== #{label} this #{commit.change_type_title}
+ %h3.page-title== #{label} this #{commit.change_type_title(current_user)}
.modal-body
- = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-#{type}-form js-requires-input' do
+ = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do
.form-group.branch
= label_tag 'target_branch', target_label, class: 'control-label'
.col-sm-10
@@ -23,12 +23,11 @@
- if can?(current_user, :push_code, @project)
.js-create-merge-request-container
.checkbox
- - nonce = SecureRandom.hex
- = label_tag "create_merge_request-#{nonce}" do
- = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}"
+ = label_tag do
+ = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: nil
Start a <strong>new merge request</strong> with these changes
- else
- = hidden_field_tag 'create_merge_request', 1
+ = hidden_field_tag 'create_merge_request', 1, id: nil
.form-actions
= submit_tag label, class: 'btn btn-create'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml
deleted file mode 100644
index 6bb900e3fc1..00000000000
--- a/app/views/projects/commit/_ci_stage.html.haml
+++ /dev/null
@@ -1,15 +0,0 @@
-%tr
- %th{colspan: 10}
- %strong
- %a{name: stage}
- - status = statuses.latest.status
- %span{class: "ci-status-link ci-status-icon-#{status}"}
- = ci_icon_for_status(status)
- - if stage
- &nbsp;
- = stage.titleize
- = render statuses.latest_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true
- = render statuses.retried_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true
- %tr
- %td{colspan: 10}
- &nbsp;
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 0ebc38d16cf..c08ed8f6c16 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -1,13 +1,8 @@
-.commit-info-row.commit-info-row-header
- .commit-meta
- %strong Commit
- %strong.monospace.js-details-short= @commit.short_id
- = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
- %span.text-expander
- \...
- %span.js-details-content.hide
- %strong.monospace.commit-hash-full= @commit.id
+.page-content-header
+ .header-main-content
+ %strong
= clipboard_button(clipboard_text: @commit.id)
+ = @commit.short_id
%span.hidden-xs authored
#{time_ago_with_tooltip(@commit.authored_date)}
%span by
@@ -19,7 +14,8 @@
%strong
= commit_committer_link(@commit, avatar: true, size: 24)
#{time_ago_with_tooltip(@commit.committed_date)}
- .commit-action-buttons
+
+ .header-action-buttons
- if defined?(@notes_count) && @notes_count > 0
%span.btn.disabled.btn-grouped.hidden-xs.append-right-10
= icon('comment')
@@ -55,8 +51,8 @@
%pre.commit-description
= preserve(markdown(@commit.description, pipeline: :single_line, author: @commit.author))
-.commit-info-widget
- .widget-row.branch-info
+.info-well
+ .well-segment.branch-info
.icon-container.commit-icon
= custom_icon("icon_commit")
%span.cgray= pluralize(@commit.parents.count, "parent")
@@ -66,8 +62,8 @@
%i.fa.fa-spinner.fa-spin
- if @commit.status
- .widget-row.pipeline-info
- .icon-container
+ .well-segment.pipeline-info
+ %div{class: "icon-container ci-status-icon-#{@commit.status}"}
= 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"
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index d6916fb7f1a..08d3443b3d0 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -1,10 +1,6 @@
.pipeline-graph-container
.row-content-block.build-content.middle-block.pipeline-actions
.pull-right
- .btn.btn-grouped.btn-white.toggle-pipeline-btn
- %span.toggle-btn-text Hide
- %span pipeline graph
- %span.caret
- if can?(current_user, :update_pipeline, pipeline.project)
- if pipeline.builds.latest.failed.any?(&:retryable?)
= link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post
@@ -28,20 +24,8 @@
in
= time_interval_in_words pipeline.duration
- .row-content-block.build-content.middle-block.pipeline-graph.hidden
- .pipeline-visualization
- %ul.stage-column-list
- - stages = pipeline.stages_with_latest_statuses
- - stages.each do |stage, statuses|
- %li.stage-column
- .stage-name
- %a{name: stage}
- - if stage
- = stage.titleize
- .builds-container
- %ul
- = render "projects/commit/pipeline_stage", statuses: statuses
-
+ .row-content-block.build-content.middle-block.js-pipeline-graph.hidden
+ = render "projects/pipelines/graph", pipeline: pipeline
- if pipeline.yaml_errors.present?
.bs-callout.bs-callout-danger
@@ -66,5 +50,4 @@
- if pipeline.project.build_coverage_enabled?
%th Coverage
%th
- - pipeline.statuses.relevant.stages.each do |stage|
- = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage)
+ = render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage
diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml
deleted file mode 100644
index 289aa5178b1..00000000000
--- a/app/views/projects/commit/_pipeline_stage.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-- status_groups = statuses.group_by(&:group_name)
-- status_groups.each do |group_name, grouped_statuses|
- - if grouped_statuses.one?
- - status = grouped_statuses.first
- - is_playable = status.playable? && can?(current_user, :update_build, @project)
- %li.build{ class: ("playable" if is_playable) }
- .curve
- .build-content
- = render "projects/#{status.to_partial_path}_pipeline", subject: status
- - else
- %li.build
- .curve
- .dropdown.inline.build-content
- = render "projects/commit/pipeline_status_group", name: group_name, subject: grouped_statuses
diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml
deleted file mode 100644
index 18daa2ee693..00000000000
--- a/app/views/projects/commit/_pipeline_status_group.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-- group_status = CommitStatus.where(id: subject).status
-%button.dropdown-menu-toggle.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } }
- %span.ci-status-icon
- = ci_icon_for_status(group_status)
- %span.ci-status-text
- = name
- %span.badge= subject.size
-.dropdown-menu.grouped-pipeline-dropdown
- .arrow
- %ul
- - subject.each do |status|
- %li
- = render "projects/#{status.to_partial_path}_pipeline", subject: status
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 2dc91a9b762..7f42fde0fea 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -12,4 +12,4 @@
%th Stages
%th
%th
- = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, show_commit: false
+ = render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 9f80a974d64..a940515fadf 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -6,7 +6,7 @@
- note_count = notes.user.count
- cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count]
-- cache_key.push(commit.status) if commit.status
+- cache_key.push(commit.status(ref)) if commit.status(ref)
= cache(cache_key, expires_in: 1.day) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
@@ -36,7 +36,6 @@
%pre.commit-row-description.js-toggle-content
= preserve(markdown(commit.description, pipeline: :single_line, author: commit.author))
- .commit-row-info
- = commit_author_link(commit, avatar: false, size: 24)
- authored
- #{time_ago_with_tooltip(commit.committed_date)}
+ = commit_author_link(commit, avatar: false, size: 24)
+ committed
+ #{time_ago_with_tooltip(commit.committed_date)}
diff --git a/app/views/projects/cycle_analytics/_empty_stage.html.haml b/app/views/projects/cycle_analytics/_empty_stage.html.haml
new file mode 100644
index 00000000000..b200ce22970
--- /dev/null
+++ b/app/views/projects/cycle_analytics/_empty_stage.html.haml
@@ -0,0 +1,7 @@
+.empty-stage-container
+ .empty-stage
+ .icon-no-data
+ = custom_icon ('icon_no_data')
+ %h4 We don’t have enough data to show this stage.
+ %p
+ {{currentStage.emptyStageText}}
diff --git a/app/views/projects/cycle_analytics/_no_access.html.haml b/app/views/projects/cycle_analytics/_no_access.html.haml
new file mode 100644
index 00000000000..0ffc79b3181
--- /dev/null
+++ b/app/views/projects/cycle_analytics/_no_access.html.haml
@@ -0,0 +1,7 @@
+.no-access-stage-container
+ .no-access-stage
+ .icon-lock
+ = custom_icon ('icon_lock')
+ %h4 You need permission.
+ %p
+ Want to see the data? Please ask administrator for access.
diff --git a/app/views/projects/cycle_analytics/_overview.html.haml b/app/views/projects/cycle_analytics/_overview.html.haml
new file mode 100644
index 00000000000..c8f0b547f80
--- /dev/null
+++ b/app/views/projects/cycle_analytics/_overview.html.haml
@@ -0,0 +1,15 @@
+.cycle-analytics-overview
+ .container
+ .row
+ .col-md-10.col-md-offset-1
+ .row.overview-details
+ .col-md-6.overview-text
+ %h4 Introducing Cycle Analytics
+ %p
+ Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
+ To set up CA, you must first define a production environment by setting up your CI and then deploy to production.
+ %p
+ %a.btn{ href: help_page_path('user/project/cycle_analytics'), target: "_blank" } Read more
+ .col-md-6.overview-image
+ %span.overview-icon
+ = custom_icon ('icon_cycle_analytics_overview')
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index 247d612ba6f..ef1b38d5e21 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -1,40 +1,35 @@
- @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_tag("cycle_analytics/cycle_analytics_bundle.js")
= render "projects/pipelines/head"
-#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) }}
-
- .bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"}
- = icon('times', class: 'dismiss-icon', "@click" => "dismissLanding()")
- .row
- .col-sm-3.col-xs-12.svg-container
- = custom_icon('icon_cycle_analytics_splash')
- .col-sm-8.col-xs-12.inner-content
- %h4
- Introducing Cycle Analytics
- %p
- Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
-
- = link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn'
+#cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } }
+ - if @cycle_analytics_no_data
+ .bordered-box.landing.content-block{"v-if" => "!isOverviewDialogDismissed"}
+ = icon("times", class: "dismiss-icon", "@click" => "dismissOverviewDialog()")
+ .row
+ .col-sm-3.col-xs-12.svg-container
+ = custom_icon('icon_cycle_analytics_splash')
+ .col-sm-8.col-xs-12.inner-content
+ %h4
+ Introducing Cycle Analytics
+ %p
+ Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
+ = link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn'
= icon("spinner spin", "v-show" => "isLoading")
-
.wrapper{"v-show" => "!isLoading && !hasError"}
.panel.panel-default
.panel-heading
Pipeline Health
-
.content-block
.container-fluid
.row
- .col-sm-3.col-xs-12.column{"v-for" => "item in analytics.summary"}
+ .col-sm-3.col-xs-12.column{"v-for" => "item in state.summary"}
%h3.header {{item.value}}
%p.text {{item.title}}
-
.col-sm-3.col-xs-12.column
.dropdown.inline.js-ca-dropdown
%button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"}
@@ -42,22 +37,54 @@
%i.fa.fa-chevron-down
%ul.dropdown-menu.dropdown-menu-align-right
%li
- %a{'href' => "#", 'data-value' => '30'}
+ %a{ "href" => "#", "data-value" => "30" }
Last 30 days
%li
- %a{'href' => "#", 'data-value' => '90'}
+ %a{ "href" => "#", "data-value" => "90" }
Last 90 days
-
- .bordered-box
- %ul.content-list
- %li{"v-for" => "item in analytics.stats"}
- .container-fluid
- .row
- .col-xs-8.title-col
- %p.title
- {{item.title}}
- %p.text
- {{item.description}}
- .col-xs-4.value-col
- %span
- {{item.value}}
+ .stage-panel-container
+ .panel.panel-default.stage-panel
+ .panel-heading
+ %nav.col-headers
+ %ul
+ %li.stage-header
+ %span.stage-name
+ Stage
+ %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The phase of the development lifecycle.", "aria-hidden" => "true" }
+ %li.median-header
+ %span.stage-name
+ Median
+ %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.", "aria-hidden" => "true" }
+ %li.event-header
+ %span.stage-name
+ {{ currentStage ? currentStage.legend : 'Related Issues' }}
+ %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The collection of events added to the data gathered for that stage.", "aria-hidden" => "true" }
+ %li.total-time-header
+ %span.stage-name
+ Total Time
+ %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The time taken by each data entry gathered by that stage.", "aria-hidden" => "true" }
+ .stage-panel-body
+ %nav.stage-nav
+ %ul
+ %li.stage-nav-item{ ':class' => '{ active: stage.active }', '@click' => 'selectStage(stage)', "v-for" => "stage in state.stages" }
+ .stage-nav-item-cell.stage-name
+ {{ stage.title }}
+ .stage-nav-item-cell.stage-median
+ %template{ "v-if" => "stage.isUserAllowed" }
+ %span{ "v-if" => "stage.value" }
+ {{ stage.value }}
+ %span.stage-empty{ "v-else" => true }
+ Not enough data
+ %template{ "v-else" => true }
+ %span.not-available
+ Not available
+ .section.stage-events
+ %template{ "v-if" => "isLoadingStage" }
+ = icon("spinner spin")
+ %template{ "v-if" => "currentStage && !currentStage.isUserAllowed" }
+ = render partial: "no_access"
+ %template{ "v-else" => true }
+ %template{ "v-if" => "isEmptyStage && !isLoadingStage" }
+ = render partial: "empty_stage"
+ %template{ "v-if" => "state.events.length && !isLoadingStage && !isEmptyStage" }
+ %component{ ":is" => "currentStage.component", ":stage" => "currentStage", ":items" => "state.events" }
diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml
index 779c8ea0104..6120b2191dd 100644
--- a/app/views/projects/diffs/_content.html.haml
+++ b/app/views/projects/diffs/_content.html.haml
@@ -9,7 +9,7 @@
- if !project.repository.diffable?(blob)
.nothing-here-block This diff was suppressed by a .gitattributes entry.
- elsif diff_file.collapsed?
- - url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path))
+ - url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier))
.nothing-here-block.diff-collapsed{data: { diff_for_path: url } }
This diff is collapsed.
%a.click-to-expand
@@ -25,7 +25,7 @@
- elsif diff_file.renamed_file
.nothing-here-block File moved
- elsif blob.image?
- - old_blob = diff_file.old_blob(diff_commit)
+ - old_blob = diff_file.old_blob(diff_file.old_content_commit || @base_commit)
= render "projects/diffs/image", diff_file: diff_file, old_file: old_blob, file: blob
- else
.nothing-here-block No preview for this file type
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 067cf595da3..ab4a2dc36e5 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -22,11 +22,12 @@
= render 'projects/diffs/warning', diff_files: diff_files
.files{ data: { can_create_note: can_create_note } }
- - diff_files.each_with_index do |diff_file, index|
+ - diff_files.each_with_index do |diff_file|
- diff_commit = commit_for_diff(diff_file)
- blob = diff_file.blob(diff_commit)
- next unless blob
- blob.load_all_data!(diffs.project.repository) unless blob.only_display_raw?
+ - file_hash = hexdigest(diff_file.file_path)
- = render 'projects/diffs/file', index: index, project: diffs.project,
+ = render 'projects/diffs/file', file_hash: file_hash, project: diffs.project,
diff_file: diff_file, diff_commit: diff_commit, blob: blob
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 8f4f9ad4a80..6c33d80becd 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,6 +1,6 @@
-.diff-file.file-holder{id: "diff-#{index}", data: diff_file_html_data(project, diff_file.file_path, diff_commit.id)}
+.diff-file.file-holder{id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_commit.id)}
.file-title{id: "file-path-#{hexdigest(diff_file.file_path)}"}
- = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "#diff-#{index}"
+ = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}"
- unless diff_file.submodule?
.file-actions.hidden-xs
@@ -9,7 +9,7 @@
= icon('comment')
\
- if editable_diff?(diff_file)
- - link_opts = @merge_request.id ? { from_merge_request_id: @merge_request.id } : {}
+ - link_opts = @merge_request.persisted? ? { from_merge_request_iid: @merge_request.iid } : {}
= edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
blob: blob, link_opts: link_opts)
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index 7042e9f1fc9..16c96b66714 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -25,9 +25,9 @@
%a{href: "##{line_code}", data: { linenumber: link_text }}
%td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }<
- if email
- %pre= diff_line_content(line.text, type)
+ %pre= line.text
- else
- = diff_line_content(line.text, type)
+ = diff_line_content(line.text)
- discussions = local_assigns.fetch(:discussions, nil)
- if discussions && !line.meta?
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index e751dabdf99..66d6254aa1e 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -9,28 +9,29 @@
%strong.cred #{diff_files.sum(&:removed_lines)} deletions
.file-stats.js-toggle-content.hide
%ul
- - diff_files.each_with_index do |diff_file, i|
+ - diff_files.each do |diff_file|
+ - file_hash = hexdigest(diff_file.file_path)
%li
- if diff_file.deleted_file
%span.deleted-file
- %a{href: "#diff-#{i}"}
+ %a{href: "##{file_hash}"}
%i.fa.fa-minus
= diff_file.old_path
- elsif diff_file.renamed_file
%span.renamed-file
- %a{href: "#diff-#{i}"}
+ %a{href: "##{file_hash}"}
%i.fa.fa-minus
= diff_file.old_path
&rarr;
= diff_file.new_path
- elsif diff_file.new_file
%span.new-file
- %a{href: "#diff-#{i}"}
+ %a{href: "##{file_hash}"}
%i.fa.fa-plus
= diff_file.new_path
- else
%span.edit-file
- %a{href: "#diff-#{i}"}
+ %a{href: "##{file_hash}"}
%i.fa.fa-adjust
= diff_file.new_path
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 0aa8801c2d8..38e7fc4279c 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -92,14 +92,15 @@
= project_feature_access_select(:wiki_access_level)
- if Gitlab.config.lfs.enabled && current_user.admin?
- .checkbox
- = f.label :lfs_enabled do
- = f.check_box :lfs_enabled
- %strong LFS
- %br
- %span.descr
+ .row
+ .col-md-9
+ = f.label :lfs_enabled, 'LFS', class: 'label-light'
+ %span.help-block
Git Large File Storage
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+ .col-md-3
+ = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control', data: { field: 'lfs_enabled' }
+
- if Gitlab.config.registry.enabled
.form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) }
@@ -111,7 +112,8 @@
%span.descr Enable Container Registry for this project
= link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank'
- = render 'merge_request_settings', f: f
+ = render 'merge_request_settings', form: f
+
%hr
%fieldset.features.append-bottom-default
%h5.prepend-top-0
@@ -144,7 +146,7 @@
such as compressing file revisions and removing unreachable objects.
.col-lg-9
= link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project),
- method: :post, class: "btn btn-save"
+ method: :post, class: "btn btn-default"
%hr
.row.prepend-top-default
.col-lg-3
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 7a39064adc5..c0a83091c8c 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -16,7 +16,7 @@
%p
Otherwise you can start with adding a
= succeed ',' do
- = link_to "README", new_readme_path, class: 'underlined-link'
+ = link_to "README", add_special_file_path(@project, file_name: 'README.md'), class: 'underlined-link'
a
= succeed ',' do
= link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE'), class: 'underlined-link'
diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml
deleted file mode 100644
index b75d5df4150..00000000000
--- a/app/views/projects/environments/_environment.html.haml
+++ /dev/null
@@ -1,35 +0,0 @@
-- last_deployment = environment.last_deployment
-
-%tr.environment
- %td
- = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment)
-
- %td.deployment-column
- - if last_deployment
- %span ##{last_deployment.iid}
- - if last_deployment.user
- by
- = user_avatar(user: last_deployment.user, size: 20)
-
- %td
- - if last_deployment && last_deployment.deployable
- = link_to [@project.namespace.becomes(Namespace), @project, last_deployment.deployable], class: 'build-link' do
- = "#{last_deployment.deployable.name} (##{last_deployment.deployable.id})"
-
- %td
- - if last_deployment
- = render 'projects/deployments/commit', deployment: last_deployment
- - else
- %p.commit-title
- No deployments yet
-
- %td
- - if last_deployment
- #{time_ago_with_tooltip(last_deployment.created_at)}
-
- %td.hidden-xs
- .pull-right
- = render 'projects/environments/external_url', environment: environment
- = render 'projects/deployments/actions', deployment: last_deployment
- = render 'projects/environments/stop', environment: environment
- = render 'projects/deployments/rollback', deployment: last_deployment
diff --git a/app/views/projects/environments/_header_title.html.haml b/app/views/projects/environments/_header_title.html.haml
deleted file mode 100644
index e056fccad5d..00000000000
--- a/app/views/projects/environments/_header_title.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-- header_title project_title(@project, "Environments", project_environments_path(@project))
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 8f555afcf11..a65a630f2d0 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -2,47 +2,21 @@
- page_title "Environments"
= render "projects/pipelines/head"
-%div{ class: container_class }
- .top-area
- %ul.nav-links
- %li{class: ('active' if @scope.nil?)}
- = link_to project_environments_path(@project) do
- Available
- %span.badge.js-available-environments-count
- = number_with_delimiter(@all_environments.available.count)
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag("environments/environments_bundle.js")
+.commit-icon-svg.hidden
+ = custom_icon("icon_commit")
+.play-icon-svg.hidden
+ = custom_icon("icon_play")
- %li{class: ('active' if @scope == 'stopped')}
- = link_to project_environments_path(@project, scope: :stopped) do
- Stopped
- %span.badge.js-stopped-environments-count
- = number_with_delimiter(@all_environments.stopped.count)
-
- - if can?(current_user, :create_environment, @project) && !@all_environments.blank?
- .nav-controls
- = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
- New environment
-
- .environments-container
- - if @all_environments.blank?
- .blank-state.blank-state-no-icon
- %h2.blank-state-title
- You don't have any environments right now.
- %p.blank-state-text
- Environments are places where code gets deployed, such as staging or production.
- %br
- = succeed "." do
- = link_to "Read more about environments", help_page_path("ci/environments")
- - if can?(current_user, :create_environment, @project)
- = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
- New environment
- - else
- .table-holder
- %table.table.ci-table.environments
- %tbody
- %th Environment
- %th Last Deployment
- %th Build
- %th Commit
- %th
- %th.hidden-xs
- = render @environments
+#environments-list-view{ data: { environments_data: environments_list_data,
+ "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
+ "can-read-environment" => can?(current_user, :read_environment, @project).to_s,
+ "can-create-environment" => can?(current_user, :create_environment, @project).to_s,
+ "project-environments-path" => project_environments_path(@project),
+ "project-stopped-environments-path" => project_environments_path(@project, scope: :stopped),
+ "new-environment-path" => new_namespace_project_environment_path(@project.namespace, @project),
+ "help-page-path" => help_page_path("ci/environments"),
+ "css-class" => container_class,
+ "commit-icon-svg" => custom_icon("icon_commit"),
+ "play-icon-svg" => custom_icon("icon_play")}}
diff --git a/app/views/projects/forks/error.html.haml b/app/views/projects/forks/error.html.haml
index 3d0ab5b85d6..98d81308407 100644
--- a/app/views/projects/forks/error.html.haml
+++ b/app/views/projects/forks/error.html.haml
@@ -13,7 +13,11 @@
- if @forked_project && @forked_project.errors.any?
%p
&ndash;
- = @forked_project.errors.full_messages.first
+ - error = @forked_project.errors.full_messages.first
+ - if error.include?("already been taken")
+ Name has already been taken
+ - else
+ = error
%p
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork", class: "btn" do
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index abf4f697f86..5ee3979c7e7 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -9,13 +9,13 @@
spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' }
.dropdown
- %button.dropdown-toggle.btn.sort-forks{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'}
%span.light sort:
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- = icon('caret-down')
+ = icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
- excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id]
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index 80fe6be49b0..9f444f076c0 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -1,39 +1,67 @@
-%tr.generic_commit_status
+- admin = local_assigns.fetch(:admin, false)
+- ref = local_assigns.fetch(:ref, nil)
+- commit_sha = local_assigns.fetch(:commit_sha, nil)
+- retried = local_assigns.fetch(:retried, false)
+- pipeline_link = local_assigns.fetch(:pipeline_link, false)
+- stage = local_assigns.fetch(:stage, false)
+- coverage = local_assigns.fetch(:coverage, false)
+
+%tr.generic_commit_status{class: ('retried' if retried)}
%td.status
- - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
- = ci_status_with_icon(generic_commit_status.status, generic_commit_status.target_url)
- - else
- = ci_status_with_icon(generic_commit_status.status)
+ = render 'ci/status/badge', status: generic_commit_status.detailed_status(current_user)
%td.generic_commit_status-link
- if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
= link_to generic_commit_status.target_url do
- %strong ##{generic_commit_status.id}
+ %span.build-link ##{generic_commit_status.id}
- else
- %strong ##{generic_commit_status.id}
+ %span.build-link ##{generic_commit_status.id}
+
+ - if ref
+ - if generic_commit_status.ref
+ .icon-container
+ = generic_commit_status.tags.any? ? icon('tag') : icon('code-fork')
+ = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref)
+ - else
+ .light none
+ .icon-container.commit-icon
+ = custom_icon("icon_commit")
+
+ - if commit_sha
+ = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "commit-id monospace"
- - if defined?(retried) && retried
+ - if retried
= icon('warning', class: 'text-warning has-tooltip', title: 'Status was retried.')
- - if defined?(commit_sha) && commit_sha
- %td
- = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace"
+ .label-container
+ - if generic_commit_status.tags.any?
+ - generic_commit_status.tags.each do |tag|
+ %span.label.label-primary
+ = tag
+ - if retried
+ %span.label.label-warning retried
- - if defined?(ref) && ref
+ - if pipeline_link
%td
- - if generic_commit_status.ref
- = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref)
+ = link_to pipeline_path(generic_commit_status.pipeline) do
+ %span.pipeline-id ##{generic_commit_status.pipeline.id}
+ %span by
+ - if generic_commit_status.pipeline.user
+ = user_avatar(user: generic_commit_status.pipeline.user, size: 20)
- else
- .light none
+ %span.monospace API
- - if defined?(runner) && runner
+ - if admin
+ %td
+ - if generic_commit_status.project
+ = link_to generic_commit_status.project.name_with_namespace, admin_namespace_project_path(generic_commit_status.project.namespace, generic_commit_status.project)
%td
- if generic_commit_status.try(:runner)
= runner_link(generic_commit_status.runner)
- else
.light none
- - if defined?(stage) && stage
+ - if stage
%td
= generic_commit_status.stage
@@ -41,24 +69,19 @@
= generic_commit_status.name
%td
- - if generic_commit_status.tags.any?
- - generic_commit_status.tags.each do |tag|
- %span.label.label-primary
- = tag
- - if defined?(retried) && retried
- %span.label.label-warning retried
-
- %td.duration
- if generic_commit_status.duration
- = icon("clock-o")
- = time_interval_in_words(generic_commit_status.duration)
+ %p.duration
+ = custom_icon("icon_timer")
+ = duration_in_numbers(generic_commit_status.duration)
- %td.timestamp
- if generic_commit_status.finished_at
- = icon("calendar")
- %span #{time_ago_with_tooltip(generic_commit_status.finished_at)}
+ %p.finished-at
+ = icon("calendar")
+ %span #{time_ago_with_tooltip(generic_commit_status.finished_at)}
- - if defined?(coverage) && coverage
- %td.coverage
- - if generic_commit_status.try(:coverage)
- #{generic_commit_status.coverage}%
+ %td.coverage
+ - if coverage && generic_commit_status.try(:coverage)
+ #{generic_commit_status.coverage}%
+
+ %td
+ -# empty column to match number of columns in ci/builds/_build.html.haml
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
deleted file mode 100644
index 1c457244a7a..00000000000
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } }
- - if subject.target_url
- = link_to subject.target_url do
- %span.ci-status-icon
- = ci_icon_for_status(subject.status)
- %span.ci-status-text= subject.name
- - else
- %span.ci-status-icon
- = ci_icon_for_status(subject.status)
- %span.ci-status-text= subject.name
diff --git a/app/views/projects/group_links/update.js.haml b/app/views/projects/group_links/update.js.haml
index af9a5b19060..55520fda494 100644
--- a/app/views/projects/group_links/update.js.haml
+++ b/app/views/projects/group_links/update.js.haml
@@ -1,3 +1,4 @@
:plain
var $listItem = $('#{escape_javascript(render('shared/members/group', group_link: @group_link))}');
$("#group_member_#{@group_link.id} .list-item-name").replaceWith($listItem.find('.list-item-name'));
+ gl.utils.localTimeAgo($('.js-timeago'), $("#group_member_#{@group_link.id}"));
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index 4d8ee562e6a..c52b3860636 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -1,4 +1,4 @@
-- page_title "Import in progress"
+- page_title @project.forked? ? "Forking in progress" : "Import in progress"
.save-project-loader
.center
%h2
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index a4b752ad86d..34d5a3e1831 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -1,8 +1,7 @@
%ul.content-list.issues-list.issuable-list
= render partial: "projects/issues/issue", collection: @issues
- if @issues.blank?
- %li
- .nothing-here-block No issues to show
+ = render 'shared/empty_states/issues'
- if @issues.present?
= paginate @issues, theme: "gitlab"
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index 31d3ec23276..d48923b422a 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -2,12 +2,12 @@
%h2.merge-requests-title
= pluralize(@merge_requests.count, 'Related Merge Request')
%ul.unstyled-list.related-merge-requests
- - has_any_ci = @merge_requests.any?(&:pipeline)
+ - has_any_ci = @merge_requests.any?(&:head_pipeline)
- @merge_requests.each do |merge_request|
%li
%span.merge-request-ci-status
- - if merge_request.pipeline
- = render_pipeline_status(merge_request.pipeline)
+ - if merge_request.head_pipeline
+ = render_pipeline_status(merge_request.head_pipeline)
- elsif has_any_ci
= icon('blank fw')
%span.merge-request-id
@@ -19,11 +19,17 @@
in
- project = merge_request.target_project
= link_to project.name_with_namespace, namespace_project_path(project.namespace, project)
- %span.merge-request-status.prepend-left-10
- - if merge_request.merged?
- MERGED
- - elsif merge_request.closed?
- CLOSED
+
+ - if merge_request.merged?
+ %span.merge-request-status.prepend-left-10.merged
+ Merged
+ - elsif merge_request.closed?
+ %span.merge-request-status.prepend-left-10.closed
+ Closed
+ - else
+ %span.merge-request-status.prepend-left-10.open
+ Open
+
- if @closed_by_merge_requests.present?
%li
= render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count}
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index c493ff3585b..26f3f0ac292 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -10,8 +10,8 @@
- if current_user
= auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@project.name} issues")
-%div{ class: (container_class) }
- - if @project.issues.any?
+- if project_issues(@project).exists?
+ %div{ class: (container_class) }
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls
@@ -36,21 +36,5 @@
= render 'issues'
- if new_issue_email
= render 'issue_by_email', email: new_issue_email
- - else
- .blank-state.blank-state-welcome
- %h2.blank-state-title.blank-state-welcome-title
- Welcome to GitLab Issues
- %p.blank-state-text
- Code, test, and deploy together
- .blank-state
- .blank-state-icon
- = custom_icon("issues", size: 50)
- %h3.blank-state-title
- You don't have any issues right now.
- %p.blank-state-text
- Issues are the best way to track your project progress
- - if can? current_user, :create_issue, @project
- = link_to new_namespace_project_issue_path(@project.namespace, @project), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do
- New Issue
- - if new_issue_email
- = render 'issue_by_email', email: new_issue_email
+- else
+ = render 'shared/empty_states/issues', button_path: new_namespace_project_issue_path(@project.namespace, @project)
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index bd629b5c519..981bf640a6b 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,3 +1,4 @@
+- @content_class = "limit-container-width"
- page_title "#{@issue.title} (#{@issue.to_reference})", "Issues"
- page_description @issue.description
- page_card_attributes @issue.card_attributes
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 12408068834..b3c43286a50 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -1,4 +1,4 @@
-%li{ class: mr_css_classes(merge_request) }
+%li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } }
- if @bulk_edit
.issue-check
= check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
@@ -15,9 +15,9 @@
= icon('ban')
CLOSED
- - if merge_request.pipeline
+ - if merge_request.head_pipeline
%li
- = render_pipeline_status(merge_request.pipeline)
+ = render_pipeline_status(merge_request.head_pipeline)
- if merge_request.open? && merge_request.broken?
%li
@@ -39,7 +39,7 @@
= icon('thumbs-down')
= downvotes
- - note_count = merge_request.mr_and_commit_notes.user.count
+ - 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')
@@ -54,15 +54,18 @@
= link_to namespace_project_commits_path(merge_request.project.namespace, merge_request.project, merge_request.target_branch) do
= icon('code-fork')
= merge_request.target_branch
+
- if merge_request.milestone
&nbsp;
= link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do
= icon('clock-o')
= merge_request.milestone.title
+
- if merge_request.labels.any?
&nbsp;
- merge_request.labels.each do |label|
= link_to_label(label, subject: merge_request.project, type: :merge_request)
+
- if merge_request.tasks?
&nbsp;
%span.task-status
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 9c6f562f7db..4a08ed045f4 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -34,10 +34,11 @@
= link_to url_for(params), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
Pipelines
%span.badge= @pipelines.size
+ - if @pipeline.present?
%li.builds-tab
= link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do
Builds
- %span.badge= @statuses.size
+ %span.badge= @statuses_count
%li.diffs-tab
= link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do
Changes
@@ -48,9 +49,10 @@
= render "projects/merge_requests/show/commits"
#diffs.diffs.tab-pane
- # This tab is always loaded via AJAX
- - if @pipelines.any?
+ - if @pipeline.present?
#builds.builds.tab-pane
= render "projects/merge_requests/show/builds"
+ - if @pipelines.any?
#pipelines.pipelines.tab-pane
= render "projects/merge_requests/show/pipelines"
@@ -65,5 +67,5 @@
:javascript
var merge_request = new MergeRequest({
action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}",
- buildsLoaded: "#{@pipelines.any? ? 'true' : 'false'}"
+ buildsLoaded: "#{@pipeline.present? ? 'true' : 'false'}"
});
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index f57abe73977..7725558518f 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,3 +1,4 @@
+- @content_class = "limit-container-width"
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
@@ -31,7 +32,7 @@
%span.label-branch= source_branch_with_namespace(@merge_request)
%span into
%span.label-branch
- = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch)
+ = link_to_if @merge_request.target_branch_exists?, @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch)
- if @merge_request.open? && @merge_request.diverged_from_target_branch?
%span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind)
@@ -41,7 +42,7 @@
= render "projects/merge_requests/widget/show.html.haml"
- if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user)
- .light.prepend-top-default.append-bottom-default
+ .merge-manually.light.prepend-top-default.append-bottom-default
You can also accept this merge request manually using the
= succeed '.' do
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
@@ -53,35 +54,37 @@
%li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
Discussion
- %span.badge= @merge_request.mr_and_commit_notes.user.count
+ %span.badge= @merge_request.related_notes.user.count
- if @merge_request.source_project
%li.commits-tab
= link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
Commits
%span.badge= @commits_count
- - if @pipeline
+ - if @pipelines.any?
%li.pipelines-tab
= link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do
Pipelines
%span.badge= @pipelines.size
+ - if @pipeline.present?
%li.builds-tab
= link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do
Builds
- %span.badge= @statuses.size
+ %span.badge= @statuses_count
%li.diffs-tab
= link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
Changes
%span.badge= @merge_request.diff_size
%li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true }
%resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
- .line-resolve-all{ "v-show" => "discussionCount > 0",
- ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
- %span.line-resolve-btn.is-disabled{ type: "button",
- ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
- = render "shared/icons/icon_status_success.svg"
- %span.line-resolve-text
- {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ discussionCount | pluralize 'discussion' }} resolved
- = render "discussions/jump_to_next"
+ %div
+ .line-resolve-all{ "v-show" => "discussionCount > 0",
+ ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
+ %span.line-resolve-btn.is-disabled{ type: "button",
+ ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
+ = render "shared/icons/icon_status_success.svg"
+ %span.line-resolve-text
+ {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
+ = render "discussions/jump_to_next"
.tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes
diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml
index d9f74d2cbfb..16789f68f70 100644
--- a/app/views/projects/merge_requests/conflicts.html.haml
+++ b/app/views/projects/merge_requests/conflicts.html.haml
@@ -30,11 +30,8 @@
.diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
= render partial: "projects/merge_requests/conflicts/components/inline_conflict_lines"
.diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
- = render partial: "projects/merge_requests/conflicts/components/parallel_conflict_lines"
+ %parallel-conflict-lines{ ":file" => "file" }
%div{"v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'"}
= render partial: "projects/merge_requests/conflicts/components/diff_file_editor"
= render partial: "projects/merge_requests/conflicts/submit_form"
-
--# Components
-= render partial: 'projects/merge_requests/conflicts/components/parallel_conflict_line'
diff --git a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
index f094df7fcaa..d35c7bee163 100644
--- a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
+++ b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
@@ -5,11 +5,10 @@
%a {{line.new_line}}
%td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
%a {{line.old_line}}
- %td.line_content{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
- {{{line.richText}}}
+ %td.line_content{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader", "v-html" => "line.richText"}
%td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
%td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
%td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
- %strong {{{line.richText}}}
+ %strong{"v-html" => "line.richText"}
%button.btn{ "@click" => "handleSelected(file, line.id, line.section)" }
{{line.buttonTitle}}
diff --git a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml
deleted file mode 100644
index 5690bf7419c..00000000000
--- a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%script{"id" => 'parallel-conflict-line', "type" => "text/x-template"}
- %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
- %td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
- %strong {{line.richText}}
- %button.btn{"@click" => "handleSelected(file, line.id, line.section)"}
- {{line.buttonTitle}}
- %td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
- {{line.lineNumber}}
- %td.line_content.parallel{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
- {{{line.richText}}}
diff --git a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml
deleted file mode 100644
index a8ecdf59393..00000000000
--- a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-%parallel-conflict-lines{"inline-template" => "true", ":file" => "file"}
- %table
- %tr.line_holder.parallel{"v-for" => "section in file.parallelLines"}
- %td{"is"=>"parallel-conflict-line", "v-for" => "line in section", ":line" => "line", ":file" => "file"}
diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml
index a0e12fb3f38..baa1ade5eee 100644
--- a/app/views/projects/merge_requests/show/_commits.html.haml
+++ b/app/views/projects/merge_requests/show/_commits.html.haml
@@ -1,6 +1,2 @@
-.content-block.oneline-block
- = icon("sort-amount-desc")
- Most recent commits displayed first
-
%ol#commits-list.list-unstyled
= render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
index eab48b78cb3..5cc92595fe0 100644
--- a/app/views/projects/merge_requests/show/_versions.html.haml
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -27,7 +27,7 @@
version #{version_index(merge_request_diff)}
.monospace #{short_sha(merge_request_diff.head_commit_sha)}
%small
- #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)},
+ #{number_with_delimiter(merge_request_diff.commits_count)} #{'commit'.pluralize(merge_request_diff.commits_count)},
= time_ago_with_tooltip(merge_request_diff.created_at)
- if @merge_request_diff.base_commit_sha
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index a82c846baa7..9ab7971b56c 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -1,22 +1,20 @@
- if @pipeline
.mr-widget-heading
- %w[success success_with_warnings skipped canceled failed running pending].each do |status|
- .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) }
+ .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) }
= 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
- - commit = @merge_request.diff_head_commit
= succeed "." do
= link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace"
%span.ci-coverage
- = link_to "View details", pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'pipelines'}
- elsif @merge_request.has_ci?
- # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
- - # Remove in later versions when services like Jenkins will set CI status via Commit status API
+ - # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API
.mr-widget-heading
- %w[success skipped canceled failed running pending].each do |status|
.ci_widget{class: "ci-#{status}", style: "display:none"}
@@ -29,8 +27,6 @@
= succeed "." do
= link_to commit.short_id, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, commit), class: "monospace"
%span.ci-coverage
- - if details_path = ci_build_details_path(@merge_request)
- = link_to "View details", details_path, :"data-no-turbolink" => "data-no-turbolink"
.ci_widget
= icon("spinner spin")
diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index 01314eb37d0..f4aa1609a1b 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -9,10 +9,10 @@
- if @project.archived?
= render 'projects/merge_requests/widget/open/archived'
- - elsif @merge_request.commits.blank?
- = render 'projects/merge_requests/widget/open/nothing'
- elsif @merge_request.branch_missing?
= render 'projects/merge_requests/widget/open/missing_branch'
+ - elsif @merge_request.has_no_commits?
+ = render 'projects/merge_requests/widget/open/nothing'
- elsif @merge_request.unchecked?
= render 'projects/merge_requests/widget/open/check'
- elsif @merge_request.cannot_be_merged? && !resolved_conflicts
@@ -23,18 +23,25 @@
= render 'projects/merge_requests/widget/open/merge_when_build_succeeds'
- elsif !@merge_request.can_be_merged_by?(current_user)
= render 'projects/merge_requests/widget/open/not_allowed'
- - elsif !@merge_request.mergeable_ci_state?
+ - elsif !@merge_request.mergeable_ci_state? && (@pipeline.failed? || @pipeline.canceled?)
= render 'projects/merge_requests/widget/open/build_failed'
- elsif !@merge_request.mergeable_discussions_state?
= render 'projects/merge_requests/widget/open/unresolved_discussions'
- elsif @merge_request.can_be_merged? || resolved_conflicts
= render 'projects/merge_requests/widget/open/accept'
- - if mr_closes_issues.present?
+ - if mr_closes_issues.present? || mr_issues_mentioned_but_not_closing.present?
.mr-widget-footer
%span
- %i.fa.fa-check
- Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)}
- = succeed '.' do
- != markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author
- = mr_assign_issues_link
+ = icon('check')
+ - if mr_closes_issues.present?
+ Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)}
+ = succeed '.' do
+ != markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author
+ = mr_assign_issues_link
+ - if mr_issues_mentioned_but_not_closing.present?
+ #{"Issue".pluralize(mr_issues_mentioned_but_not_closing.size)}
+ != markdown issues_sentence(mr_issues_mentioned_but_not_closing), pipeline: :gfm, author: @merge_request.author
+ #{mr_issues_mentioned_but_not_closing.size > 1 ? 'are' : 'is'} mentioned but will not closed.
+
+
diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml
index 608fdf1c5f5..a8918c85dde 100644
--- a/app/views/projects/merge_requests/widget/_show.html.haml
+++ b/app/views/projects/merge_requests/widget/_show.html.haml
@@ -14,7 +14,7 @@
ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
ci_environments_status_url: "#{ci_environments_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
gitlab_icon: "#{asset_path 'gitlab_logo.png'}",
- ci_status: "#{@merge_request.pipeline ? @merge_request.pipeline.status : ''}",
+ ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}",
ci_message: {
normal: "Build {{status}} for \"{{title}}\"",
preparing: "{{status}} build for \"{{title}}\""
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 ce43ca3a286..d6f7f23533c 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -9,7 +9,7 @@
- if @pipeline && @pipeline.active?
%span.btn-group
= button_tag class: "btn btn-create js-merge-button merge_when_build_succeeds" do
- Merge When Build Succeeds
+ Merge When Pipeline Succeeds
- unless @project.only_allow_merge_if_build_succeeds?
= button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do
= icon('caret-down')
@@ -19,7 +19,7 @@
%li
= link_to "#", class: "merge_when_build_succeeds" do
= icon('check fw')
- Merge When Build Succeeds
+ Merge When Pipeline Succeeds
%li
= link_to "#", class: "accept_merge_request" do
= icon('warning fw')
@@ -41,6 +41,8 @@
Modify commit message
.js-toggle-content.hide.prepend-top-default
= render 'shared/commit_message_container', params: params,
+ message_with_description: @merge_request.merge_commit_message(include_description: true),
+ message_without_description: @merge_request.merge_commit_message,
text: @merge_request.merge_commit_message,
rows: 14, hint: true
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 2b6b5e05e86..072d01d144e 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,6 +1,6 @@
%h4
Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)}
- to be merged automatically when the build succeeds.
+ to be merged automatically when the pipeline succeeds.
%div
%p
= succeed '.' do
@@ -21,5 +21,5 @@
Remove Source Branch When Merged
- if user_can_cancel_automatic_merge
- = link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-warning btn-sm" do
+ = link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-sm" do
Cancel Automatic Merge
diff --git a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml
index 35d5677ee37..e094f97f3b6 100644
--- a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml
@@ -3,4 +3,8 @@
This merge request has unresolved discussions
%p
- Please resolve these discussions to allow this merge request to be merged. \ No newline at end of file
+ Please resolve these discussions
+ - if @project.issues_enabled? && can?(current_user, :create_issue, @project)
+ or
+ = link_to "open an issue to resolve them later", new_namespace_project_issue_path(@project.namespace, @project, merge_request_for_resolving_discussions: @merge_request.iid)
+ to allow this merge request to be merged.
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index cbf1ba04170..513710e8e66 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -14,12 +14,7 @@
= render 'projects/notes/hints'
.clearfix
.error-alert
- .col-md-6
- .form-group
- = f.label :due_date, "Due Date", class: "control-label"
- .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
+ = render "shared/milestones/form_dates", f: f
.form-actions
- if @milestone.new_record?
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index f9ba77e87b5..c3a6096aa54 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -4,21 +4,24 @@
= render "projects/issues/head"
%div{ class: container_class }
- .detail-page-header
+ .detail-page-header.milestone-page-header
.status-box{ class: status_box_class(@milestone) }
- if @milestone.closed?
Closed
- elsif @milestone.expired?
Past due
+ - elsif @milestone.upcoming?
+ Upcoming
- else
Open
- %span.identifier
- Milestone ##{@milestone.iid}
- - if @milestone.expires_at
- %span.creator
- &middot;
- = @milestone.expires_at
- .pull-right
+ .header-text-content
+ %span.identifier
+ Milestone ##{@milestone.iid}
+ - if @milestone.due_date || @milestone.start_date
+ %span.creator
+ &middot;
+ = milestone_date_range(@milestone)
+ .milestone-buttons
- if can?(current_user, :admin_milestone, @project)
- if @milestone.active?
= link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 932603f03b0..0788924d44a 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -90,7 +90,8 @@
= f.label :visibility_level, class: 'label-light' do
Visibility Level
= link_to "(?)", help_page_path("public_access/public_access")
- = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: @project.visibility_level, form_model: @project)
+ = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project
+
= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel'
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index ab719e38904..778a32e6345 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -14,6 +14,9 @@
= note.author.to_reference
- unless note.system
commented
+ - if note.system
+ %span{class: 'system-note-message'}
+ = note.redacted_note_html
%a{ href: "##{dom_id(note)}" }
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- unless note.system?
@@ -32,7 +35,7 @@
"resolved-by" => "#{note.resolved_by.try(:name)}",
"v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true,
- "v-ref:note_#{note.id}" => true }
+ "ref" => "note_#{note.id}" }
.note-action-button
= icon("spin spinner", "v-show" => "loading")
@@ -43,7 +46,7 @@
"@click" => "resolve",
":title" => "buttonText",
"v-show" => "!loading",
- "v-el:button" => true }
+ ":ref" => "'button'" }
= render "shared/icons/icon_status_success.svg"
@@ -62,12 +65,14 @@
.note-text.md
= preserve do
= note.redacted_note_html
- = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
+ = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable
= render 'projects/notes/edit_form', note: note
.note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false
-
+ - if note.system
+ .system-note-commit-list-toggler
+ Toggle commit list
- if note.attachment.url
.note-attachment
- if note.attachment.image?
diff --git a/app/views/projects/pipelines/_graph.html.haml b/app/views/projects/pipelines/_graph.html.haml
new file mode 100644
index 00000000000..0202833c0bf
--- /dev/null
+++ b/app/views/projects/pipelines/_graph.html.haml
@@ -0,0 +1,4 @@
+- pipeline = local_assigns.fetch(:pipeline)
+.pipeline-visualization.pipeline-graph
+ %ul.stage-column-list
+ = render partial: "projects/stage/graph", collection: pipeline.stages, as: :stage
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index d288efc546f..b00ba2d5307 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -1,39 +1,47 @@
-%p
-.commit-info-row
- Pipeline
- = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @pipeline.id), class: "monospace"
- with
- = pluralize @pipeline.statuses.count(:id), "build"
- - if @pipeline.ref
- for
- = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
- - if @pipeline.duration
- in
- = time_interval_in_words(@pipeline.duration)
- - if @pipeline.queued_duration
- = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
-
- .pull-right
- = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do
- = ci_icon_for_status(@pipeline.status)
- = ci_label_for_status(@pipeline.status)
-
-- if @commit
- .commit-info-row
- %span.light Authored by
- %strong
- = commit_author_link(@commit, avatar: true, size: 24)
- #{time_ago_with_tooltip(@commit.authored_date)}
-
-.commit-info-row
- %span.light Commit
- = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace"
- = clipboard_button(clipboard_text: @pipeline.sha)
+.page-content-header
+ .header-main-content
+ = render 'ci/status/badge', status: @pipeline.detailed_status(current_user)
+ %strong Pipeline ##{@commit.pipelines.last.id}
+ triggered #{time_ago_with_tooltip(@commit.authored_date)} by
+ = author_avatar(@commit, size: 24)
+ = commit_author_link(@commit)
+ .header-action-buttons
+ - if can?(current_user, :update_pipeline, @pipeline.project)
+ - if @pipeline.builds.latest.failed.any?(&:retryable?)
+ = link_to "Retry failed", retry_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'btn btn-inverted-secondary', method: :post
+ - if @pipeline.builds.running_or_pending.any?
+ = link_to "Cancel running", cancel_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
- if @commit
- .commit-box.content-block
+ .commit-box
%h3.commit-title
= markdown(@commit.title, pipeline: :single_line)
- if @commit.description.present?
%pre.commit-description
= preserve(markdown(@commit.description, pipeline: :single_line))
+
+.info-well
+ - if @commit.status
+ .well-segment.pipeline-info
+ %div{class: "icon-container ci-status-icon-#{@commit.status}"}
+ = ci_icon_for_status(@commit.status)
+ = pluralize @pipeline.statuses.count(:id), "build"
+ - if @pipeline.ref
+ from
+ = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
+ - if @pipeline.duration
+ in
+ = time_interval_in_words(@pipeline.duration)
+ - if @pipeline.queued_duration
+ = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
+
+ .well-segment.branch-info
+ .icon-container.commit-icon
+ = custom_icon("icon_commit")
+ = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace js-details-short"
+ = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
+ %span.text-expander
+ \...
+ %span.js-details-content.hide
+ = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace commit-hash-full"
+ = clipboard_button(clipboard_text: @pipeline.sha, title: "Copy commit SHA to clipboard")
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
new file mode 100644
index 00000000000..88af41aa835
--- /dev/null
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -0,0 +1,42 @@
+.tabs-holder
+ %ul.pipelines-tabs.nav-links.no-top.no-bottom
+ %li.js-pipeline-tab-link
+ = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: { target: 'div#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do
+ 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
+ %span.badge.js-builds-counter= pipeline.statuses.count
+
+
+
+.tab-content
+ #js-tab-pipeline.tab-pane
+ .build-content.middle-block.js-pipeline-graph
+ = render "projects/pipelines/graph", pipeline: pipeline
+
+ #js-tab-builds.tab-pane
+ - if pipeline.yaml_errors.present?
+ .bs-callout.bs-callout-danger
+ %h4 Found errors in your .gitlab-ci.yml:
+ %ul
+ - pipeline.yaml_errors.split(",").each do |error|
+ %li= error
+ You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path}
+
+ - if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file
+ .bs-callout.bs-callout-warning
+ \.gitlab-ci.yml not found in this commit
+
+ .table-holder.pipeline-holder
+ %table.table.ci-table.pipeline
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Name
+ %th
+ - if pipeline.project.build_coverage_enabled?
+ %th Coverage
+ %th
+ = render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 4bc49072f35..030cd8ef78f 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -5,23 +5,23 @@
%div{ class: container_class }
.top-area
%ul.nav-links
- %li{class: ('active' if @scope.nil?)}
+ %li{class: ('active' if @scope.nil?)}>
= link_to project_pipelines_path(@project) do
All
%span.badge.js-totalbuilds-count
= number_with_delimiter(@pipelines_count)
- %li{class: ('active' if @scope == 'running')}
+ %li{class: ('active' if @scope == 'running')}>
= link_to project_pipelines_path(@project, scope: :running) do
Running
%span.badge.js-running-count
= number_with_delimiter(@running_or_pending_count)
- %li{class: ('active' if @scope == 'branches')}
+ %li{class: ('active' if @scope == 'branches')}>
= link_to project_pipelines_path(@project, scope: :branches) do
Branches
- %li{class: ('active' if @scope == 'tags')}
+ %li{class: ('active' if @scope == 'tags')}>
= link_to project_pipelines_path(@project, scope: :tags) do
Tags
@@ -37,7 +37,6 @@
%span CI Lint
%div.content-list.pipelines
- - stages = @pipelines.stages
- if @pipelines.blank?
%div
.nothing-here-block No pipelines to show
@@ -51,6 +50,6 @@
%th Stages
%th
%th.hidden-xs
- = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages
+ = render @pipelines, commit_sha: true, stage: true, allow_retry: true
= paginate @pipelines, theme: 'gitlab'
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 688535ad764..29a41bc664b 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -2,10 +2,8 @@
- page_title "Pipeline"
= render "projects/pipelines/head"
-%div{ class: container_class }
- .prepend-top-default
- - if @commit
- = render "projects/pipelines/info"
- %div.block-connector
+%div.js-pipeline-container{ class: container_class, data: { controller_action: "#{controller.action_name}" } }
+ - if @commit
+ = render "projects/pipelines/info"
- = render "projects/commit/pipeline", pipeline: @pipeline
+ = render "projects/pipelines/with_tabs", pipeline: @pipeline
diff --git a/app/views/projects/pipelines_settings/_badge.html.haml b/app/views/projects/pipelines_settings/_badge.html.haml
index 7b7fa56d993..22a3b884520 100644
--- a/app/views/projects/pipelines_settings/_badge.html.haml
+++ b/app/views/projects/pipelines_settings/_badge.html.haml
@@ -1,4 +1,4 @@
-.row{ class: badge.title.gsub(' ', '-') }
+%div{ class: badge.title.gsub(' ', '-') }
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
= badge.title.capitalize
diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml
index 96221a20502..1f698558bce 100644
--- a/app/views/projects/pipelines_settings/show.html.haml
+++ b/app/views/projects/pipelines_settings/show.html.haml
@@ -86,6 +86,9 @@
%li
tap --coverage-report=text-summary (NodeJS) -
%code ^Statements\s*:\s*([^%]+)
+ %li
+ excoveralls (Elixir) -
+ %code \[TOTAL\]\s+(\d+\.\d+)%
= f.submit 'Save changes', class: "btn btn-save"
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index bdeb704b6da..4f1cec20f85 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -21,6 +21,7 @@
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
%button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
= icon("search")
+ = render 'shared/members/sort_dropdown'
- if @group_links.any?
= render 'groups', group_links: @group_links
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
index 91927181efb..d15f4310ff5 100644
--- a/app/views/projects/project_members/update.js.haml
+++ b/app/views/projects/project_members/update.js.haml
@@ -1,3 +1,4 @@
:plain
var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}');
$("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
+ gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@project_member)}"));
diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml
index 1141168f037..d07bb661615 100644
--- a/app/views/projects/refs/logs_tree.js.haml
+++ b/app/views/projects/refs/logs_tree.js.haml
@@ -14,5 +14,8 @@
// Load more commit logs for each file in tree
// if we still on the same page
var url = "#{escape_javascript(@more_log_url)}";
- ajaxGet(url);
+ gl.utils.ajaxGet(url);
}
+
+:plain
+ gl.utils.localTimeAgo($('.js-timeago', 'table.table_#{@hex_path} tbody'));
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index b41edeb2c7e..db51c4f8a4e 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -7,14 +7,15 @@
%p= @service.description
.col-lg-9
= form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form|
- = render 'shared/service_settings', form: form
+ = render 'shared/service_settings', form: form, subject: @service
- = form.submit 'Save changes', class: 'btn btn-save'
- &nbsp;
- - if @service.valid? && @service.activated?
- - unless @service.can_test?
- - disabled_class = 'disabled'
- - disabled_title = @service.disabled_title
+ .footer-block.row-content-block
+ = form.submit 'Save changes', class: 'btn btn-save'
+ &nbsp;
+ - if @service.valid? && @service.activated?
+ - unless @service.can_test?
+ - disabled_class = 'disabled'
+ - disabled_title = @service.disabled_title
- = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled_class}", title: disabled_title
- = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel"
+ = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled_class}", title: disabled_title
+ = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel"
diff --git a/app/views/projects/services/index.html.haml b/app/views/projects/services/index.html.haml
index 4a33a5bc6f6..66fd3029dc9 100644
--- a/app/views/projects/services/index.html.haml
+++ b/app/views/projects/services/index.html.haml
@@ -28,5 +28,6 @@
%td.hidden-xs
= service.description
%td.light
- = time_ago_in_words service.updated_at
- ago
+ - if service.updated_at.present?
+ = time_ago_in_words service.updated_at
+ ago
diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
new file mode 100644
index 00000000000..a676c0290a0
--- /dev/null
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -0,0 +1,100 @@
+- pretty_path_with_namespace = "#{@project ? @project.namespace.name : 'namespace'} / #{@project ? @project.name : 'name'}"
+- run_actions_text = "Perform common operations on this project: #{pretty_path_with_namespace}"
+
+.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
+ %br
+ %br
+ To setup this service:
+ %ul.list-unstyled
+ %li
+ 1.
+ = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands'
+ 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:
+
+ %hr
+
+ .help-form
+ .form-group
+ = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :display_name, "GitLab / #{pretty_path_with_namespace}", class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#display_name')
+
+ .form-group
+ = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#description')
+
+ .form-group
+ = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block
+ %p Fill in the word that works best for your team.
+ %p
+ Suggestions:
+ %code= 'gitlab'
+ %code= @project.path # Path contains no spaces, but dashes
+ %code= @project.path_with_namespace
+
+ .form-group
+ = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#request_url')
+
+ .form-group
+ = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block POST
+
+ .form-group
+ = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#response_username')
+
+ .form-group
+ = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#response_icon')
+
+ .form-group
+ = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block Yes
+
+ .form-group
+ = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#autocomplete_hint')
+
+ .form-group
+ = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#autocomplete_description')
+
+ %hr
+
+ %ul.list-unstyled
+ %li
+ 3. After adding the slash command, paste the
+ %strong token
+ into the field below
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 4de95036eef..c50093cf47c 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -66,8 +66,8 @@
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
Set Up CI
- %li.project-repo-buttons-right
- .project-repo-buttons.project-right-buttons
+ %li.project-repo-buttons.right
+ .project-right-buttons
- if current_user
= render 'shared/members/access_request_buttons', source: @project
= render "projects/buttons/koding"
@@ -76,7 +76,8 @@
= render 'projects/buttons/download', project: @project, ref: @ref
= render 'projects/buttons/dropdown'
- = render 'shared/notifications/button', notification_setting: @notification_setting
+ .pull-right
+ = render 'shared/notifications/button', notification_setting: @notification_setting
- if @repository.commit
.project-last-commit{ class: container_class }
= render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index 32e1f8a21b0..068a6610350 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -1,13 +1,13 @@
.hidden-xs
- - if can?(current_user, :create_project_snippet, @project)
- = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New snippet" do
- New snippet
- - if can?(current_user, :update_project_snippet, @snippet)
- = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
- Delete
- if can?(current_user, :update_project_snippet, @snippet)
- = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do
+ = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped" do
Edit
+ - if can?(current_user, :update_project_snippet, @snippet)
+ = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do
+ Delete
+ - 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 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" } }
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index e77e1b026f6..84e05cd6d88 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -1,11 +1,19 @@
- page_title "Snippets"
-.sub-header-block
- - if can?(current_user, :create_project_snippet, @project)
- = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do
- New snippet
+- if current_user
+ .top-area
+ - include_private = @project.team.member?(current_user) || current_user.admin?
+ = render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private }
+
+ .nav-controls.hidden-xs
+ - if can?(current_user, :create_project_snippet, @project)
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New snippet" do
+ New snippet
- .oneline
- Share code pastes with others out of git repository
+- if can?(current_user, :create_project_snippet, @project)
+ .visible-xs
+ &nbsp;
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-block", title: "New snippet" do
+ New snippet
= render 'snippets/snippets'
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 9503dbded13..79d87b7db12 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -8,10 +8,11 @@
= blob_icon 0, @snippet.file_name
= @snippet.file_name
.file-actions
- = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
+ = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm")
= link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
= render 'shared/snippets/blob'
- = render 'award_emoji/awards_block', awardable: @snippet, inline: true
+ .row-content-block.top-block.content-component-block
+ = render 'award_emoji/awards_block', awardable: @snippet, inline: true
%div#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml
new file mode 100644
index 00000000000..b70b574e687
--- /dev/null
+++ b/app/views/projects/stage/_graph.html.haml
@@ -0,0 +1,21 @@
+- stage = local_assigns.fetch(:stage)
+- statuses = stage.statuses.latest
+- status_groups = statuses.sort_by(&:name).group_by(&:group_name)
+%li.stage-column
+ .stage-name
+ %a{ name: stage.name }
+ = stage.name.titleize
+ .builds-container
+ %ul
+ - status_groups.each do |group_name, grouped_statuses|
+ - if grouped_statuses.one?
+ - status = grouped_statuses.first
+ %li.build
+ .curve
+ .build-content
+ = render 'ci/status/graph_badge', subject: status
+ - else
+ %li.build
+ .curve
+ .dropdown.inline.build-content
+ = render 'projects/stage/in_stage_group', name: group_name, subject: grouped_statuses
diff --git a/app/views/projects/stage/_in_stage_group.html.haml b/app/views/projects/stage/_in_stage_group.html.haml
new file mode 100644
index 00000000000..b03837d1211
--- /dev/null
+++ b/app/views/projects/stage/_in_stage_group.html.haml
@@ -0,0 +1,13 @@
+- group_status = CommitStatus.where(id: subject).status
+%button.dropdown-menu-toggle.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } }
+ %span{class: "ci-status-icon ci-status-icon-#{group_status}"}
+ = ci_icon_for_status(group_status)
+ %span.ci-status-text
+ = name
+ %span.dropdown-counter-badge= subject.size
+.dropdown-menu.grouped-pipeline-dropdown
+ .arrow
+ %ul
+ - subject.each do |status|
+ %li.dropdown-build
+ = render 'ci/status/graph_badge', subject: status
diff --git a/app/views/projects/stage/_stage.html.haml b/app/views/projects/stage/_stage.html.haml
new file mode 100644
index 00000000000..1684e02fbad
--- /dev/null
+++ b/app/views/projects/stage/_stage.html.haml
@@ -0,0 +1,13 @@
+%tr
+ %th{colspan: 10}
+ %strong
+ %a{ name: stage.name }
+ %span{class: "ci-status-link ci-status-icon-#{stage.status}"}
+ = ci_icon_for_status(stage.status)
+ &nbsp;
+ = stage.name.titleize
+= render stage.statuses.latest_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, allow_retry: true
+= render stage.statuses.retried_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, retried: true
+%tr
+ %td{colspan: 10}
+ &nbsp;
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 7a0d9dcc94f..1d39f3a7534 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -10,15 +10,16 @@
.nav-controls
= form_tag(filter_tags_path, method: :get) do
= search_field_tag :search, params[:search], { placeholder: 'Filter by tag name', id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false }
+
.dropdown.inline
- %button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} }
+ %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown'} }
%span.light
- = @sort.humanize
- = icon('caret-down')
+ = projects_sort_options_hash[@sort]
+ = icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
- = link_to filter_tags_path(sort: nil) do
- Name
+ = link_to filter_tags_path(sort: sort_value_name) do
+ = sort_title_name
= link_to filter_tags_path(sort: sort_value_recently_updated) do
= sort_title_recently_updated
= link_to filter_tags_path(sort: sort_value_oldest_updated) do
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 3a097750d6e..c06a413eb2f 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -23,7 +23,7 @@
= label_tag :message, nil, class: 'control-label'
.col-sm-10
= text_area_tag :message, nil, required: false, tabindex: 3, class: 'form-control', rows: 5
- .help-block Optionally, enter a message to create an annotated tag.
+ .help-block Optionally, add a message to the tag.
%hr
.form-group
= label_tag :release_description, 'Release notes', class: 'control-label'
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 155af755759..12facb6eb73 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -3,8 +3,16 @@
= render "projects/commits/head"
%div{ class: container_class }
- .sub-header-block
- .pull-right.tag-buttons
+ .top-area.multi-line
+ .nav-text
+ .title
+ %span.item-title= @tag.name
+ - if @commit
+ = render 'projects/branches/commit', commit: @commit, project: @project
+ - else
+ Cant find HEAD commit for this tag
+
+ .nav-controls
- if can?(current_user, :push_code, @project)
= link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Edit release notes' do
= icon("pencil")
@@ -15,15 +23,8 @@
= render 'projects/buttons/download', project: @project, ref: @tag.name
- if can?(current_user, :admin_project, @project)
.pull-right
- = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
+ = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
%i.fa.fa-trash-o
- .tag-info.append-bottom-10
- .title
- %span.item-title= @tag.name
- - if @commit
- = render 'projects/branches/commit', commit: @commit, project: @project
- - else
- Cant find HEAD commit for this tag
- if @tag.message.present?
%pre.body
= strip_gpg_signature(@tag.message)
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index 21e378b8735..d37c376c36b 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -5,14 +5,11 @@
%tr
%th Name
%th.hidden-xs
- .pull-left Last Commit
+ .pull-left Last commit
.last-commit.hidden-sm.pull-left
- &nbsp;
- %i.fa.fa-angle-right
- &nbsp;
- %small.light
+ %small.light
+ = clipboard_button(clipboard_text: @commit.id)
= link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
- &ndash;
= time_ago_with_tooltip(@commit.committed_date)
= @commit.full_title
%small.commit-history-link-spacer &#124;
diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml
index f6e0b0a7c8a..6e5dd1b196d 100644
--- a/app/views/projects/triggers/index.html.haml
+++ b/app/views/projects/triggers/index.html.haml
@@ -76,6 +76,16 @@
script:
- "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}"
%h5.prepend-top-default
+ Use webhook
+
+ %p.light
+ Add the following webhook to another project for Push and Tag push events.
+ The project will be rebuilt at the corresponding event.
+
+ %pre
+ :plain
+ #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN
+ %h5.prepend-top-default
Pass build variables
%p.light
@@ -83,10 +93,18 @@
%code variables[VARIABLE]=VALUE
to an API request. Variable values can be used to distinguish between triggered builds and normal builds.
- %pre.append-bottom-0
+ With cURL:
+
+ %pre
:plain
curl -X POST \
-F token=TOKEN \
-F "ref=REF_NAME" \
-F "variables[RUN_NIGHTLY_BUILD]=true" \
#{builds_trigger_url(@project.id)}
+ %p.light
+ With webhook:
+
+ %pre.append-bottom-0
+ :plain
+ #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN&variables[RUN_NIGHTLY_BUILD]=true
diff --git a/app/views/projects/variables/_table.html.haml b/app/views/projects/variables/_table.html.haml
index 07cee86ba4c..c7cebf45160 100644
--- a/app/views/projects/variables/_table.html.haml
+++ b/app/views/projects/variables/_table.html.haml
@@ -12,8 +12,8 @@
- @project.variables.order_key_asc.each do |variable|
- if variable.id?
%tr
- %td= variable.key
- %td= variable.value
+ %td.variable-key= variable.key
+ %td.variable-value{ "data-value" => variable.value }******
%td
= link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-edit" do
%span.sr-only
diff --git a/app/views/projects/variables/index.html.haml b/app/views/projects/variables/index.html.haml
index 09bb54600af..39303700131 100644
--- a/app/views/projects/variables/index.html.haml
+++ b/app/views/projects/variables/index.html.haml
@@ -15,3 +15,4 @@
No variables found, add one with the form above.
- else
= render "table"
+ %button.btn.btn-info.js-btn-toggle-reveal-values{"data-status" => 'hidden'} Reveal Values
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 4e41a15d9f4..c52527332bc 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -34,9 +34,6 @@
- if @page && @page.persisted?
= f.submit 'Save changes', class: "btn-save btn"
.pull-right
- - if can?(current_user, :admin_wiki, @project)
- = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger btn-grouped" do
- Delete
= link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-cancel btn-grouped"
- else
= f.submit 'Create page', class: "btn-create btn"
diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml
deleted file mode 100644
index 09c4411d67e..00000000000
--- a/app/views/projects/wikis/_nav.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-= content_for :sub_nav do
- .scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
- = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
-
- = nav_link(path: 'wikis#pages') do
- = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project)
-
- = nav_link(path: 'wikis#git_access') do
- = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
- Git Access
-
- = render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
new file mode 100644
index 00000000000..cad9c15a49e
--- /dev/null
+++ b/app/views/projects/wikis/_sidebar.html.haml
@@ -0,0 +1,23 @@
+%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar
+ .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')
+
+ - git_access_url = namespace_project_wikis_git_access_path(@project.namespace, @project)
+ = link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '' do
+ = succeed '&nbsp;' do
+ = icon('cloud-download')
+ Clone repository
+
+ .blocks-container
+ .block.block-first
+ %ul.wiki-pages
+ - @sidebar_wiki_pages.each do |wiki_page|
+ %li{ class: params[:id] == wiki_page.slug ? 'active' : '' }
+ = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do
+ = wiki_page.title.capitalize
+ .block
+ = link_to namespace_project_wikis_pages_path(@project.namespace, @project), class: 'btn btn-block' do
+ More Pages
+
+= render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 679d6018bef..8cf018da1b7 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,23 +1,35 @@
- @no_container = true
- page_title "Edit", @page.title.capitalize, "Wiki"
-= render 'nav'
%div{ class: container_class }
- .top-area
+ .wiki-page-header.has-sidebar-toggle
+ %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
+ = icon('angle-double-left')
+
.nav-text
- %strong
+ %h2.wiki-page-title
- if @page.persisted?
= link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
- else
= @page.title.capitalize
- %span.light
- &middot;
- Edit Page
+ %span.light
+ &middot;
+ - if @page.persisted?
+ Edit Page
+ - else
+ Create Page
.nav-controls
- - if !(@page && @page.persisted?)
- - if can?(current_user, :create_wiki, @project)
- = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
- New Page
+ - if can?(current_user, :create_wiki, @project)
+ = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
+ New Page
+ - if @page.persisted?
+ = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do
+ Page History
+ - if can?(current_user, :admin_wiki, @project)
+ = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger" do
+ Delete
= render 'form'
+
+= render 'sidebar'
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index b8811a28dd6..e25d6a48573 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -1,34 +1,36 @@
- @no_container = true
- page_title "Git Access", "Wiki"
-= render 'nav'
%div{ class: container_class }
- .sub-header-block
- %span.oneline
- Git access for
+ .wiki-page-header.has-sidebar-toggle
+ %button.btn.btn-default.visible-xs.visible-sm.pull-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
+ = icon('angle-double-left')
+
+ .git-access-header
+ Clone repository
%strong= @project_wiki.path_with_namespace
- .pull-right
- = render "shared/clone_panel", project: @project_wiki
+ = render "shared/clone_panel", project: @project_wiki
+
+ .wiki-git-access
+ %h3 Install Gollum
+ %pre.dark
+ :preserve
+ gem install gollum
- .prepend-top-default
- %fieldset
- %legend Install Gollum:
- %pre.dark
- :preserve
- gem install gollum
+ %h3 Clone your wiki
+ %pre.dark
+ :preserve
+ git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')}
+ cd #{h @project_wiki.path}
- %legend Clone Your Wiki:
- %pre.dark
- :preserve
- git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')}
- cd #{h @project_wiki.path}
+ %h3 Start Gollum and edit locally
+ %pre.dark
+ :preserve
+ gollum
+ == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin
+ >> Thin web server (v1.5.0 codename Knife)
+ >> Maximum connections set to 1024
+ >> Listening on 0.0.0.0:4567, CTRL+C to stop
- %legend Start Gollum And Edit Locally:
- %pre.dark
- :preserve
- gollum
- == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin
- >> Thin web server (v1.5.0 codename Knife)
- >> Maximum connections set to 1024
- >> Listening on 0.0.0.0:4567, CTRL+C to stop
+= render 'sidebar'
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index 4c0b14e2c42..dd7213622c1 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -1,13 +1,16 @@
- page_title "History", @page.title.capitalize, "Wiki"
-= render 'nav'
+
%div{ class: container_class }
- .top-area
+ .wiki-page-header.has-sidebar-toggle
+ %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
+ = icon('angle-double-left')
+
.nav-text
- %strong
+ %h2.wiki-page-title
= link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
- %span.light
- &middot;
- History
+ %span.light
+ &middot;
+ History
.table-holder
%table.table
@@ -35,3 +38,5 @@
%td
%strong
= @page.page.wiki.page(@page.page.name, commit.id).try(:format)
+
+= render 'sidebar'
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index 9c10acd4cb6..e1eaffc6884 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -1,9 +1,18 @@
- @no_container = true
- page_title "Pages", "Wiki"
-= render 'nav'
-
%div{ class: container_class }
+ .wiki-page-header
+
+ .nav-text
+ %h2.wiki-page-title
+ Wiki Pages
+
+ .nav-controls
+ = link_to namespace_project_wikis_git_access_path(@project.namespace, @project), class: 'btn' do
+ = icon('cloud-download')
+ Clone repository
+
%ul.content-list
- @wiki_pages.each do |wiki_page|
%li
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 5cebb538cf5..1b6dceee241 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -1,15 +1,19 @@
- @no_container = true
- page_title @page.title.capitalize, "Wiki"
-= render 'nav'
%div{ class: container_class }
- .top-area
+ .wiki-page-header.has-sidebar-toggle
+ %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
+ = icon('angle-double-left')
+
.nav-text
- %strong= @page.title.capitalize
+ %h2.wiki-page-title= @page.title.capitalize
%span.wiki-last-edit-by
- &middot;
- last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
+ Last edited by
+ %strong
+ #{@page.commit.author.name}
+ #{time_ago_with_tooltip(@page.commit.authored_date)}
.nav-controls
= render 'main_links'
@@ -19,8 +23,9 @@
This is an old version of this page.
You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}.
-
.wiki-holder.prepend-top-default.append-bottom-default
.wiki
= preserve do
= render_wiki_content(@page)
+
+= render 'sidebar'
diff --git a/app/views/repository_check_mailer/notify.html.haml b/app/views/repository_check_mailer/notify.html.haml
index a585147ddd1..94e5a5d9709 100644
--- a/app/views/repository_check_mailer/notify.html.haml
+++ b/app/views/repository_check_mailer/notify.html.haml
@@ -2,7 +2,7 @@
#{@message}.
%p
- = link_to "See the affected projects in the GitLab admin panel", admin_namespaces_projects_url(last_repository_check_failed: 1)
+ = link_to "See the affected projects in the GitLab admin panel", admin_projects_url(last_repository_check_failed: 1)
%p
You are receiving this message because you are a GitLab administrator for #{Gitlab.config.gitlab.url}.
diff --git a/app/views/repository_check_mailer/notify.text.haml b/app/views/repository_check_mailer/notify.text.haml
index 93db151329e..0902c50d052 100644
--- a/app/views/repository_check_mailer/notify.text.haml
+++ b/app/views/repository_check_mailer/notify.text.haml
@@ -1,6 +1,6 @@
#{@message}.
\
-View details: #{admin_namespaces_projects_url(last_repository_check_failed: 1)}
+View details: #{admin_projects_url(last_repository_check_failed: 1)}
You are receiving this message because you are a GitLab administrator
for #{Gitlab.config.gitlab.url}.
diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml
index ef1c0296d49..938be20c7cf 100644
--- a/app/views/search/_filter.html.haml
+++ b/app/views/search/_filter.html.haml
@@ -3,7 +3,7 @@
- if params[:project_id].present?
= hidden_field_tag :project_id, params[:project_id]
.dropdown
- %button.dropdown-menu-toggle.btn.js-search-group-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Group:" } }
+ %button.dropdown-menu-toggle.js-search-group-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Group:" } }
%span.dropdown-toggle-text
Group:
- if @group.present?
@@ -18,7 +18,7 @@
= dropdown_loading
.dropdown.project-filter
- %button.dropdown-menu-toggle.btn.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Project:" } }
+ %button.dropdown-menu-toggle.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Project:" } }
%span.dropdown-toggle-text
Project:
- if @project.present?
diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml
index 6f0a0ea36ec..9e8adc82583 100644
--- a/app/views/search/results/_blob.html.haml
+++ b/app/views/search/results/_blob.html.haml
@@ -1,11 +1,13 @@
-- blob = parse_search_result(blob)
+- file_name, blob = blob
.blob-result
.file-holder
.file-title
- - blob_link = namespace_project_blob_path(@project.namespace, @project, tree_join(blob.ref, blob.filename))
+ - ref = @search_results.repository_ref
+ - blob_link = namespace_project_blob_path(@project.namespace, @project, tree_join(ref, file_name))
= link_to blob_link do
%i.fa.fa-file
%strong
- = blob.filename
- .file-content.code.term
- = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link
+ = file_name
+ - if blob
+ .file-content.code.term
+ = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link
diff --git a/app/views/search/results/_commit.html.haml b/app/views/search/results/_commit.html.haml
index 5b2d83d6b92..f34eaf89027 100644
--- a/app/views/search/results/_commit.html.haml
+++ b/app/views/search/results/_commit.html.haml
@@ -1 +1 @@
-= render 'projects/commits/commit', project: @project, commit: commit
+= render 'projects/commits/commit', project: @project, commit: commit, ref: nil
diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml
index c414acb6a11..027d42396b4 100644
--- a/app/views/search/results/_snippet_title.html.haml
+++ b/app/views/search/results/_snippet_title.html.haml
@@ -14,7 +14,7 @@
= link_to snippet_title.project.name_with_namespace, namespace_project_path(snippet_title.project.namespace, snippet_title.project)
.snippet-info
- = "##{snippet_title.id}"
+ = snippet_title.to_reference
%span
by
= link_to user_snippets_path(snippet_title.author) do
diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml
index 0a38327baa2..c196bc06b17 100644
--- a/app/views/shared/_commit_message_container.html.haml
+++ b/app/views/shared/_commit_message_container.html.haml
@@ -1,5 +1,6 @@
.form-group.commit_message-group
- nonce = SecureRandom.hex
+ - descriptions = local_assigns.slice(:message_with_description, :message_without_description)
= label_tag "commit_message-#{nonce}", class: 'control-label' do
Commit message
.col-sm-10
@@ -8,9 +9,17 @@
= text_area_tag 'commit_message',
(params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder]),
class: 'form-control js-commit-message', placeholder: local_assigns[:placeholder],
+ data: descriptions,
required: true, rows: (local_assigns[:rows] || 3),
id: "commit_message-#{nonce}"
- if local_assigns[:hint]
%p.hint
Try to keep the first line under 52 characters
and the others under 72.
+ - if descriptions.present?
+ %p.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
+ = link_to "#", class: "js-without-description-link" do
+ Don't include description in commit message
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index c367ae336db..e50ab5fea09 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -5,5 +5,7 @@
- if event_filter_visible(:merge_requests)
= event_filter_link EventFilter.merged, 'Merge events'
- if event_filter_visible(:issues)
+ = event_filter_link EventFilter.issue, 'Issue events'
+ - if comments_visible?
= event_filter_link EventFilter.comments, 'Comments'
= event_filter_link EventFilter.team, 'Team'
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index ba25e09d638..0bc851b4256 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -13,8 +13,9 @@
.input-group-addon
= root_url
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
- autofocus: local_assigns[:autofocus] || false, pattern: "[a-zA-Z0-9-_]+",
- required: true, title: 'Please choose a group name with no special characters.'
+ 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 @group.persisted?
.alert.alert-warning.prepend-top-10
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index a5df502d7b5..26b349e8a62 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -1,4 +1,4 @@
-- if @issues.reorder(nil).any?
+- if @issues.to_a.any?
- @issues.group_by(&:project).each do |group|
.panel.panel-default.panel-small
- project = group[0]
@@ -13,4 +13,4 @@
= render 'projects/issues/issue', issue: issue
= paginate @issues, theme: "gitlab"
- else
- .nothing-here-block No issues to show
+ = render 'shared/empty_states/issues'
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 6ccdef0df46..db324d8868e 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -1,6 +1,7 @@
- label_css_id = dom_id(label)
- open_issues_count = label.open_issues_count(current_user)
- open_merge_requests_count = label.open_merge_requests_count(current_user)
+- status = label_subscription_status(label, @project).inquiry if current_user
- subject = local_assigns[:subject]
%li{id: label_css_id, data: { id: label.id } }
@@ -18,10 +19,19 @@
%li
= link_to_label(label, subject: subject) do
= pluralize open_issues_count, 'open issue'
- - if current_user
- %li.label-subscription{ data: toggle_subscription_data(label) }
- %a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } }
- %span= label_subscription_toggle_button_text(label)
+ - if current_user && defined?(@project)
+ %li.label-subscription
+ - if label.is_a?(ProjectLabel)
+ %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', data: { status: status, url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
+ %span= label_subscription_toggle_button_text(label, @project)
+ - else
+ %a.js-unsubscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' if status.unsubscribed?), data: { url: group_label_unsubscribe_path(label, @project) } }
+ %span Unsubscribe
+ %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
+ %span Subscribe at project level
+ %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } }
+ %span Subscribe at group level
+
- if can?(current_user, :admin_label, label)
%li
= link_to 'Edit', edit_label_path(label)
@@ -34,12 +44,27 @@
= link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action') do
= pluralize open_issues_count, 'open issue'
- - if current_user
- .label-subscription.inline{ data: toggle_subscription_data(label) }
- %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } }
- %span.sr-only= label_subscription_toggle_button_text(label)
- = icon('eye', class: 'label-subscribe-button-icon', disabled: label.is_a?(GroupLabel))
- = icon('spinner spin', class: 'label-subscribe-button-loading')
+ - if current_user && defined?(@project)
+ .label-subscription.inline
+ - if label.is_a?(ProjectLabel)
+ %button.js-subscribe-button.label-subscribe-button.btn.btn-default.btn-action{ type: 'button', title: label_subscription_toggle_button_text(label, @project), data: { toggle: 'tooltip', status: status, url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
+ %span= label_subscription_toggle_button_text(label, @project)
+ = icon('spinner spin', class: 'label-subscribe-button-loading')
+ - else
+ %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default.btn-action{ type: 'button', class: ('hidden' if status.unsubscribed?), title: 'Unsubscribe', data: { toggle: 'tooltip', url: group_label_unsubscribe_path(label, @project) } }
+ %span Unsubscribe
+ = icon('spinner spin', class: 'label-subscribe-button-loading')
+
+ .dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) }
+ %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+ %span Subscribe
+ = icon('chevron-down')
+ %ul.dropdown-menu
+ %li
+ %a.js-subscribe-button{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
+ Project level
+ %a.js-subscribe-button{ data: { url: toggle_subscription_group_label_path(label.group, label) } }
+ Group level
- if can?(current_user, :admin_label, label)
= link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
@@ -49,6 +74,10 @@
%span.sr-only Delete
= icon('trash-o')
- - if current_user && label.is_a?(ProjectLabel)
- :javascript
- new Subscription('##{dom_id(label)} .label-subscription');
+ - if current_user && defined?(@project)
+ - if label.is_a?(ProjectLabel)
+ :javascript
+ new gl.ProjectLabelSubscription('##{dom_id(label)} .label-subscription');
+ - else
+ :javascript
+ new gl.GroupLabelSubscription('##{dom_id(label)} .label-subscription');
diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml
index ca3178395c1..2f3605b4d27 100644
--- a/app/views/shared/_merge_requests.html.haml
+++ b/app/views/shared/_merge_requests.html.haml
@@ -1,4 +1,4 @@
-- if @merge_requests.reorder(nil).any?
+- if @merge_requests.to_a.any?
- @merge_requests.group_by(&:target_project).each do |group|
.panel.panel-default.panel-small
- project = group[0]
diff --git a/app/views/shared/_milestone_expired.html.haml b/app/views/shared/_milestone_expired.html.haml
index b8eef15fbec..5e9007aaaac 100644
--- a/app/views/shared/_milestone_expired.html.haml
+++ b/app/views/shared/_milestone_expired.html.haml
@@ -1,5 +1,7 @@
- if milestone.expired? and not milestone.closed?
%span.cred (Expired)
-- if milestone.expires_at
+- if milestone.upcoming?
+ %span.clgray (Upcoming)
+- if milestone.due_date || milestone.start_date
%span
- = milestone.expires_at
+ = milestone_date_range(milestone)
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index 73d288e2236..186ed4a7c8b 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -2,17 +2,17 @@
- counts = milestone_counts(@project.milestones)
%ul.nav-links
- %li{class: milestone_class_for_state(params[:state], 'opened', true)}
+ %li{class: milestone_class_for_state(params[:state], 'opened', true)}>
= link_to milestones_filter_path(state: 'opened') do
Open
- if @project
%span.badge #{counts[:opened]}
- %li{class: milestone_class_for_state(params[:state], 'closed')}
+ %li{class: milestone_class_for_state(params[:state], 'closed')}>
= link_to milestones_filter_path(state: 'closed') do
Closed
- if @project
%span.badge #{counts[:closed]}
- %li{class: milestone_class_for_state(params[:state], 'all')}
+ %li{class: milestone_class_for_state(params[:state], 'all')}>
= link_to milestones_filter_path(state: 'all') do
All
- if @project
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index 5254d265918..9c5053dace5 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -1,46 +1,50 @@
= form_errors(@service)
-- if @service.help.present?
+- if lookup_context.template_exists?('help', "projects/services/#{@service.to_param}", true)
+ = render "projects/services/#{@service.to_param}/help", subject: subject
+- elsif @service.help.present?
.well
= preserve do
= markdown @service.help
-.form-group
- = form.label :active, "Active", class: "control-label"
- .col-sm-10
- = form.check_box :active
-
-.form-group
- = form.label :url, "Trigger", class: 'control-label'
-
- .col-sm-10
- - @service.supported_events.each do |event|
- %div
- = form.check_box service_event_field_name(event), class: 'pull-left'
- .prepend-left-20
- = form.label service_event_field_name(event), class: 'list-label' do
- %strong
- = event.humanize
-
- - field = @service.event_field(event)
-
- - if field
- %p
- = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder]
-
- %p.light
- = service_event_description(event)
-
-- @service.global_fields.each do |field|
- - type = field[:type]
-
- - if type == 'fieldset'
- - fields = field[:fields]
- - legend = field[:legend]
-
- %fieldset
- %legend= legend
- - fields.each do |subfield|
- = render 'shared/field', form: form, field: subfield
- - else
- = render 'shared/field', form: form, field: field
+.service-settings
+ .form-group
+ = form.label :active, "Active", class: "control-label"
+ .col-sm-10
+ = form.check_box :active
+
+ - if @service.supported_events.present?
+ .form-group
+ = form.label :url, "Trigger", class: 'control-label'
+
+ .col-sm-10
+ - @service.supported_events.each do |event|
+ %div
+ = form.check_box service_event_field_name(event), class: 'pull-left'
+ .prepend-left-20
+ = form.label service_event_field_name(event), class: 'list-label' do
+ %strong
+ = event.humanize
+
+ - field = @service.event_field(event)
+
+ - if field
+ %p
+ = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder]
+
+ %p.light
+ = service_event_description(event)
+
+ - @service.global_fields.each do |field|
+ - type = field[:type]
+
+ - if type == 'fieldset'
+ - fields = field[:fields]
+ - legend = field[:legend]
+
+ %fieldset
+ %legend= legend
+ - fields.each do |subfield|
+ = render 'shared/field', form: form, field: subfield
+ - else
+ = render 'shared/field', form: form, field: field
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index 68e05cb72e1..ede3c7090d7 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -1,11 +1,11 @@
.dropdown.inline.prepend-left-10
- %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-toggle{type: 'button', data: {toggle: 'dropdown'}}
%span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- = icon('caret-down')
+ = icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
%li
= link_to page_filter_path(sort: sort_value_priority, label: true) do
diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml
index 60353aee7f1..b6047ece592 100644
--- a/app/views/shared/builds/_tabs.html.haml
+++ b/app/views/shared/builds/_tabs.html.haml
@@ -1,23 +1,23 @@
%ul.nav-links
- %li{ class: ('active' if scope.nil?) }
+ %li{ class: ('active' if scope.nil?) }>
= link_to build_path_proc.call(nil) do
All
%span.badge.js-totalbuilds-count
= number_with_delimiter(all_builds.count(:id))
- %li{ class: ('active' if scope == 'pending') }
+ %li{ class: ('active' if scope == 'pending') }>
= link_to build_path_proc.call('pending') do
Pending
%span.badge
= number_with_delimiter(all_builds.pending.count(:id))
- %li{ class: ('active' if scope == 'running') }
+ %li{ class: ('active' if scope == 'running') }>
= link_to build_path_proc.call('running') do
Running
%span.badge
= number_with_delimiter(all_builds.running.count(:id))
- %li{ class: ('active' if scope == 'finished') }
+ %li{ class: ('active' if scope == 'finished') }>
= link_to build_path_proc.call('finished') do
Finished
%span.badge
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
new file mode 100644
index 00000000000..e939278bc07
--- /dev/null
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -0,0 +1,22 @@
+- button_path = local_assigns.fetch(:button_path, false)
+- project_select_button = local_assigns.fetch(:project_select_button, false)
+- has_button = button_path || project_select_button
+
+.row.empty-state
+ .pull-right.col-xs-12{ class: "#{'col-sm-6' if has_button}" }
+ .svg-content
+ = render 'shared/empty_states/icons/issues.svg'
+ .col-xs-12{ class: "#{'col-sm-6' if has_button}" }
+ .text-content
+ - if has_button
+ %h4
+ The Issue Tracker is a good place to add things that need to be improved or solved in a project!
+ %p
+ 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 project_select_button
+ = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue'
+ - else
+ = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
+ - else
+ %h4.text-center There are no issues to show.
diff --git a/app/views/shared/empty_states/icons/_issues.svg b/app/views/shared/empty_states/icons/_issues.svg
new file mode 100644
index 00000000000..2e92bf19579
--- /dev/null
+++ b/app/views/shared/empty_states/icons/_issues.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="790 253 425 254" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="25" height="8.9423" x="25" y="88.4231" rx="2"/><mask id="h" width="25" height="8.9423" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M16 29.8013h43V91.404H16z"/><mask id="i" width="43" height="61.6026" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><path id="c" d="M57 60.6026l13.1868 9.3587c.449.3188.876 1.0142.9556 1.5673l3.5747 24.863c.1564 1.0866-.253 1.2572-.912.384L66 86.436l-9-6.9552"/><mask id="j" width="17.7504" height="36.7306" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><path id="d" d="M.2496 60.6026l13.1868 9.3587c.449.3188.876 1.0142.9556 1.5673l3.5748 24.863c.1562 1.0866-.2532 1.2572-.9123.384L9.2495 86.436l-9-6.9552"/><mask id="k" width="17.7504" height="36.7306" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask><path id="e" d="M16 29.8013L35.786 1.4556c.9466-1.3562 2.4792-1.3594 3.428 0L59 29.8013"/><mask id="l" width="43" height="29.364" x="0" y="0" fill="#fff"><use xlink:href="#e"/></mask><rect id="f" width="26.2653" height="35.5088" x="6.3673" rx="13.1327"/><mask id="m" width="26.2653" height="35.5088" x="0" y="0" fill="#fff"><use xlink:href="#f"/></mask><rect id="g" width="16.8367" height="22.386" x="4.0816" rx="8.4184"/><mask id="n" width="16.8367" height="22.386" x="0" y="0" fill="#fff"><use xlink:href="#g"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(792.000000, 255.000000)"><g fill="#FDE5D8"><path d="M225.4372 59.5866c-.059.5897-.1323 1.2698-.2203 2.0305-.252 2.1764-.5717 4.559-.9653 7.07-.1283.8185.4312 1.586 1.2496 1.7143.8185.1283 1.586-.4312 1.7142-1.2497.4-2.5528.7253-4.975.9815-7.1898.0898-.7762.1646-1.4715.2252-2.0762.0366-.365.0604-.62.0722-.7557.0717-.8254-.539-1.5526-1.3645-1.6244-.8254-.0717-1.5526.539-1.6244 1.3645-.0106.1228-.0332.365-.0684.7166zM219.8738 87.9413c-.2563.7878.1745 1.6342.9622 1.8906.7878.2562 1.6342-.1745 1.8906-.9623.975-2.9962 1.849-6.2827 2.6287-9.797.1794-.8086-.3308-1.6097-1.1395-1.789-.8088-.1795-1.61.3306-1.7893 1.1394-.76 3.4256-1.6096 6.6206-2.5527 9.5183zM209.9266 103.166c-.781.2766-1.1897 1.134-.913 1.9148.2765.781 1.1338 1.1897 1.9147.913 2.9792-1.0552 5.5414-3.679 7.7796-7.6272.4084-.7207.1554-1.636-.5653-2.0447-.7207-.4086-1.636-.1556-2.0446.565-1.9152 3.3786-3.9945 5.508-6.1714 6.279zM190.439 107.5834c-.7636.3214-1.122 1.201-.8005 1.9645.3215.7634 1.201 1.1217 1.9645.8003 3.1204-1.314 6.2717-2.3243 9.258-2.9816.809-.178 1.3205-.9783 1.1424-1.7874-.178-.809-.9783-1.3205-1.7874-1.1424-3.1666.697-6.4914 1.763-9.777 3.1464zM173.231 118.6257c-.6005.5706-.6248 1.52-.0542 2.1206s1.52.625 2.1206.0543c2.282-2.1682 4.8656-4.162 7.6758-5.946.6994-.444.9064-1.371.4624-2.0704-.444-.6994-1.371-.9064-2.0704-.4624-2.9698 1.8854-5.707 3.998-8.1342 6.304zM162.4543 136.2492c-.2022.8034.2852 1.6185 1.0885 1.8207.8034.202 1.6186-.2853 1.8208-1.0886.7688-3.0547 2.0416-5.9768 3.781-8.7486.4403-.7018.2284-1.6276-.4733-2.068-.7017-.4402-1.6275-.2283-2.068.4734-1.9026 3.0322-3.3016 6.2438-4.149 9.611zM162.1894 156.693c.1036.822.854 1.4042 1.676 1.3006.8218-.1037 1.404-.854 1.3004-1.676-.367-2.9097-.5796-6.1364-.6444-9.8167-.0146-.8284-.698-1.488-1.5262-1.4734-.8283.0146-1.488.698-1.4733 1.5262.0665 3.783.286 7.1162.6674 10.1393zM168.408 176.1653c.3876.7322 1.2953 1.0117 2.0275.6242.7322-.3875 1.0117-1.2952.6242-2.0274-1.6733-3.162-2.9028-5.9954-3.8477-8.943-.2528-.789-1.0973-1.2235-1.8862-.9706-.789.2528-1.2234 1.0974-.9706 1.8863 1.0025 3.1275 2.3014 6.121 4.053 9.4306zM175.9738 188.9357c1.056 1.7165 1.8892 3.0806 2.7307 4.474.4283.709 1.3503.9368 2.0595.5085.709-.4283.9368-1.3503.5085-2.0595-.8464-1.4014-1.6836-2.772-2.7434-4.4948.0808.131-1.9545-3.1733-2.486-4.0405-.4328-.7063-1.3563-.928-2.0627-.495-.7063.4327-.928 1.3563-.495 2.0626.5334.8707 2.5708 4.1785 2.4885 4.0447zM184.83 211.3822c.011.8284.6912 1.491 1.5196 1.4803.8283-.0108 1.491-.691 1.4803-1.5194-.046-3.519-.6604-6.996-1.8367-10.3262-.276-.7812-1.1328-1.1908-1.914-.915-.781.276-1.1906 1.133-.9147 1.914 1.0668 3.0206 1.624 6.1733 1.6655 9.3664zM179.3467 229.4095c-.459.6896-.2723 1.6208.4173 2.08.6896.459 1.6208.272 2.08-.4175 1.966-2.9533 3.4756-6.124 4.4877-9.4165.2434-.7918-.2012-1.631-.993-1.8745-.792-.2434-1.6312.2012-1.8746.993-.9264 3.014-2.3108 5.922-4.1173 8.6355z"/></g><g transform="translate(336.866969, 147.225953) rotate(-300.000000) translate(-336.866969, -147.225953) translate(299.366969, 69.725953)"><path stroke="#FDE5D8" stroke-width="3" d="M19 154l10-52.6603m16 0L55 154" stroke-linecap="round"/><rect width="3" height="38.75" x="35" y="99.3526" fill="#FDE5D8" rx="1.5"/><use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#h)" xlink:href="#a"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#i)" xlink:href="#b"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#j)" xlink:href="#c"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#k)" transform="translate(9.124810, 78.967887) scale(-1, 1) translate(-9.124810, -78.967887)" xlink:href="#d"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#l)" xlink:href="#e"/><ellipse cx="28.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="34.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="40.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="46.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="37.5" cy="55.1378" stroke="#FDE5D8" stroke-width="3" rx="10.5" ry="10.4327"/><ellipse cx="37.5" cy="55.1378" stroke="#FDE5D8" stroke-width="3" rx="5.5" ry="5.4647"/></g><path fill="#EEE" d="M96.0426 37.2106c-.1512 1.6874.0814 3.815.997 6.146.2046.5207.7936.7774 1.3155.5733.522-.2043.7793-.792.5747-1.313-.7912-2.0142-.99-3.832-.865-5.226.0102-.1143.0195-.186.0238-.2113.092-.552-.2814-1.0738-.8344-1.1658-.553-.092-1.076.2808-1.168.8326-.0126.075-.0285.1975-.0434.364zM107.5302 52.8934c.4913.239 1.098.0626 1.355-.394.2572-.4566.0674-1.0205-.4238-1.2595-1.8668-.9083-3.4584-1.9152-4.7943-3.0075-.4162-.3404-1.0506-.3026-1.4168.0843-.3663.387-.3256.9766.0907 1.317 1.4583 1.1925 3.1828 2.2835 5.1893 3.2596zM120.661 58.9533c.5467.171 1.1257-.1425 1.2933-.7003.1675-.5577-.1397-1.1484-.6864-1.3194-3.0283-.9472-4.1984-1.3178-5.915-1.8824-.544-.179-1.1274.126-1.3028.6813-.1754.5552.1235 1.1504.6677 1.3294 1.729.5686 2.9053.941 5.943 1.8913zM132.5954 62.881c.449.246 1.022.0983 1.2798-.33.258-.4282.103-.975-.3458-1.221-1.4942-.819-3.1928-1.545-5.2675-2.2746-.486-.1708-1.025.0664-1.204.53-.179.4634.0697.9776.5555 1.1484 1.9832.6973 3.5892 1.3838 4.982 2.1472zM141.9774 73.383c.205.4938.809.742 1.3485.5543.5395-.1878.8106-.7404.6055-1.2344-.8504-2.0482-1.853-3.7962-3.0375-5.3046-.337-.429-.99-.527-1.4588-.2184-.4687.3085-.5755.9064-.2386 1.3354 1.0743 1.368 1.9926 2.9692 2.7808 4.8675zM144.609 87.025c.0183.5535.5682.99 1.2283.9746.66-.0153 1.1805-.4764 1.1622-1.03-.0725-2.2033-.2693-4.206-.622-6.1198-.1008-.5473-.7115-.9225-1.3642-.838-.6526.0846-1.1.597-.999 1.1442.336 1.8248.5248 3.745.5947 5.869z"/><path fill="#E5E5E5" d="M144.1423 95.7297c-.0863 2.5442-.1214 3.769-.1422 5.2548-.0076.5523.3963 1.007.9022 1.0154.506.0083.9223-.4326.93-.985.0205-1.4668.0554-2.6812.1412-5.2113l.026-.7667c.0185-.552-.3764-1.016-.882-1.0363-.5056-.0203-.9306.411-.949.963l-.026.766zM144.939 115.201c.1196.5447.6727.8925 1.2355.7768.5628-.1157.922-.651.8026-1.1957-.417-1.9-.7104-3.84-.8976-5.8637-.0513-.5545-.5574-.964-1.1305-.9142-.573.0497-.996.5396-.9448 1.0942.1944 2.1015.4998 4.121.9348 6.103zM149.995 127.5248c.296.454.9528.61 1.4668.3485.514-.2614.6907-.8413.3947-1.2952-1.0787-1.6535-2.0046-3.3145-2.7896-4.9916-.2266-.484-.8547-.7143-1.403-.5142-.548.2-.809.7546-.5823 1.2387.8208 1.7534 1.788 3.4886 2.9134 5.2138zM154.8088 135.226c1.0587 1.232 2.242 2.4097 3.543 3.531.404.3482 1.0276.3186 1.393-.066.3657-.3843.3346-.978-.0692-1.3262-1.2296-1.0597-2.345-2.17-3.3402-3.328-.195-.227-.3872-.4542-.5764-.6813-.3385-.4063-.9588-.4744-1.3856-.1522-.4267.3223-.4983.913-.1598 1.3192.1954.2346.3938.469.5952.7034zM170.634 146.9026c.4806.242 1.0517.0176 1.2758-.501.224-.5188.0162-1.1354-.4642-1.3773-1.7563-.8842-3.422-1.8432-4.9857-2.8726-.4527-.298-1.0434-.1435-1.3195.3452-.276.4885-.133 1.126.3198 1.424 1.6256 1.0704 3.354 2.0655 5.1738 2.9816z"/><path fill="#EEE" d="M184.7334 151.9698c.5527.1412 1.1072-.2262 1.2385-.8206.1312-.5944-.2104-1.1908-.763-1.332-2.001-.5114-3.9602-1.1002-5.8632-1.763-.5405-.1883-1.1205.1303-1.2955.7115-.175.5813.1212 1.205.6616 1.3934 1.9557.6813 3.9676 1.286 6.0214 1.8108zM197.9337 153.9977c.5532.04 1.0297-.445 1.0643-1.083.0346-.6383-.3857-1.188-.939-1.228-1.973-.1424-3.952-.3682-5.9206-.676-.5492-.086-1.0547.358-1.1292.9917-.0744.6336.3105 1.2168.8597 1.3027 2.0164.3154 4.0433.5467 6.0647.6927zM212.1213 152.6062c.5493-.055.9392-.4576.871-.8994-.0684-.442-.569-.7555-1.1184-.7006-1.9168.1917-3.893.3194-5.9104.382-.553.0173-.9842.392-.9628.8368.0213.445.487.7916 1.0402.7744 2.0737-.0645 4.1064-.1957 6.0803-.3932zM226.3665 149.949c.5293-.22.7755-.8162.5497-1.332-.2257-.5155-.838-.7553-1.3672-.5354-1.7815.74-3.7143 1.3827-5.7772 1.923-.5558.1454-.8852.7023-.7358 1.2436.1494.5414.721.8623 1.2768.7168 2.1547-.5643 4.1797-1.2376 6.0537-2.016zM237.8486 140.4168c.292-.4344.1488-1.006-.3202-1.2766-.469-.2706-1.086-.1378-1.3782.2967-.9575 1.4237-2.225 2.7337-3.7847 3.9202-.427.3248-.4888.9087-.138 1.3042.3505.3955.981.4528 1.408.128 1.723-1.3107 3.1363-2.7714 4.213-4.3726zM245.6725 130.6874c.3987-.3503.439-.9587.09-1.3588-.3492-.4-.9554-.4405-1.3542-.0902-1.5048 1.3222-2.8978 2.7094-4.1698 4.1635-.3497.3995-.3102 1.008.088 1.3587.3983.3508 1.0046.3113 1.3542-.0884 1.2153-1.389 2.5487-2.717 3.9918-3.985zM257.4814 122.8697c.476-.2568.657-.8577.4047-1.342-.2523-.4843-.8428-.6687-1.3188-.4118-1.7682.9542-3.4795 1.973-5.1228 3.0587-.4518.2985-.5803.9133-.287 1.373.2934.46.8975.5906 1.3494.292 1.5938-1.0528 3.2557-2.0423 4.9746-2.97zM270.276 116.9216c.5503-.1682.8513-.724.6723-1.241-.179-.5173-.77-.8003-1.3204-.632-1.9296.5898-3.932 1.2728-5.975 2.054-.536.205-.7936.7797-.5754 1.2835.218.504.8294.746 1.3654.541 1.9947-.7628 3.95-1.4298 5.833-2.0054z"/><circle cx="145" cy="90" r="5" fill="#FFF" stroke="#EEE" stroke-width="2"/><circle cx="238" cy="138" r="5" fill="#FFF" stroke="#EEE" stroke-width="2"/><path stroke="#B5A7DD" stroke-width="3" d="M20.0605 56s-17.4698 33-12 53c5.4697 20 17 32 38 44S78.5 148 107 159s29 43 29 43" stroke-linecap="round" stroke-dasharray="8 10"/><g stroke="#EEE" stroke-width="3" transform="translate(108.000000, 173.000000)"><path fill="#FFF" d="M154 77c0-42.526-34.474-77-77-77S0 34.474 0 77" stroke-linecap="round"/><circle cx="108" cy="41" r="16"/><circle cx="42.5" cy="30.5" r="8.5"/><circle cx="22" cy="58" r="5"/></g><g><g fill="#FC8A51" transform="translate(235.917801, 27.746228) rotate(-345.000000) translate(-235.917801, -27.746228) translate(216.417801, 4.246228) translate(19.897959, -0.000000)"><path d="M.398 11.2982h2.3877c0-4.234 3.3853-7.6666 7.5612-7.6666v-2.421C4.8522 1.2105.398 5.727.398 11.298z"/><ellipse cx="10.7449" cy="2.0175" rx="1.9898" ry="2.0175"/></g><g fill="#FC8A51" transform="translate(235.917801, 27.746228) rotate(-345.000000) translate(-235.917801, -27.746228) translate(216.417801, 4.246228) translate(12.602041, 6.000000) scale(-1, 1) translate(-12.602041, -6.000000) translate(6.102041, -0.000000)"><path d="M.398 11.2982h2.3877c0-4.234 3.3853-7.6666 7.5612-7.6666v-2.421C4.8522 1.2105.398 5.727.398 11.298z"/><ellipse cx="10.7449" cy="2.0175" rx="1.9898" ry="2.0175"/></g><g transform="translate(235.917801, 27.746228) rotate(-345.000000) translate(-235.917801, -27.746228) translate(216.417801, 4.246228) translate(0.000000, 10.491228)"><g fill="#FC8A51" transform="translate(29.448980, 11.298246)"><rect width="7.9592" height="2" x=".7959" y="8.8772" rx="1"/><rect width="7.9592" height="2" x=".7959" y="16.1404" transform="translate(4.775510, 17.140351) rotate(-345.000000) translate(-4.775510, -17.140351)" rx="1"/><rect width="7.9592" height="2" x=".9151" y="1.8072" transform="translate(4.894667, 2.807217) rotate(-15.000000) translate(-4.894667, -2.807217)" rx="1"/></g><g fill="#FC8A51" transform="translate(5.051020, 21.298246) scale(-1, 1) translate(-5.051020, -21.298246) translate(0.551020, 11.298246)"><rect width="7.9592" height="2" x=".7959" y="8.8772" rx="1"/><rect width="7.9592" height="2" x=".7959" y="16.1404" transform="translate(4.775510, 17.140351) rotate(-345.000000) translate(-4.775510, -17.140351)" rx="1"/><rect width="7.9592" height="2" x=".9151" y="1.8072" transform="translate(4.894667, 2.807217) rotate(-15.000000) translate(-4.894667, -2.807217)" rx="1"/></g><use stroke="#FC8A51" stroke-width="6" mask="url(#m)" xlink:href="#f"/><path fill="#FC8A51" d="M7.1633 12.9123H31.041v3H7.1632z"/></g></g><g><g fill="#EEE" transform="translate(92.956359, 18.724125) scale(-1, 1) rotate(-345.000000) translate(-92.956359, -18.724125) translate(80.456359, 3.724125) translate(12.755102, 0.000000)"><path d="M.255 7.1228h1.5307c0-2.6694 2.17-4.8333 4.847-4.8333V.7632C3.1104.7632.255 3.6105.255 7.1228z"/><ellipse cx="6.8878" cy="1.2719" rx="1.2755" ry="1.2719"/></g><g fill="#EEE" transform="translate(92.956359, 18.724125) scale(-1, 1) rotate(-345.000000) translate(-92.956359, -18.724125) translate(80.456359, 3.724125) translate(7.744898, 4.000000) scale(-1, 1) translate(-7.744898, -4.000000) translate(3.244898, 0.000000)"><path d="M.255 7.1228h1.5307c0-2.6694 2.17-4.8333 4.847-4.8333V.7632C3.1104.7632.255 3.6105.255 7.1228z"/><ellipse cx="6.8878" cy="1.2719" rx="1.2755" ry="1.2719"/></g><g transform="translate(92.956359, 18.724125) scale(-1, 1) rotate(-345.000000) translate(-92.956359, -18.724125) translate(80.456359, 3.724125) translate(0.000000, 6.614035)"><g fill="#EEE" transform="translate(18.877551, 7.122807)"><rect width="5.102" height="2" x=".5102" y="5.5965" rx="1"/><rect width="5.102" height="2" x=".5102" y="10.1754" transform="translate(3.061224, 11.175439) rotate(-345.000000) translate(-3.061224, -11.175439)" rx="1"/><rect width="5.102" height="2" x=".5866" y="1.1393" transform="translate(3.137607, 2.139333) rotate(-15.000000) translate(-3.137607, -2.139333)" rx="1"/></g><g fill="#EEE" transform="translate(3.122449, 13.622807) scale(-1, 1) translate(-3.122449, -13.622807) translate(0.122449, 7.122807)"><rect width="5.102" height="2" x=".5102" y="5.5965" rx="1"/><rect width="5.102" height="2" x=".5102" y="10.1754" transform="translate(3.061224, 11.175439) rotate(-345.000000) translate(-3.061224, -11.175439)" rx="1"/><rect width="5.102" height="2" x=".5866" y="1.1393" transform="translate(3.137607, 2.139333) rotate(-15.000000) translate(-3.137607, -2.139333)" rx="1"/></g><use stroke="#EEE" stroke-width="4" mask="url(#n)" xlink:href="#g"/><path fill="#EEE" d="M4.5918 8.1404h15.306v2H4.592z"/></g></g><g fill="#FFF" transform="translate(0.000000, 103.000000)"><circle cx="8.5" cy="8.5" r="8.5" stroke="#B5A7DD" stroke-width="4"/><circle cx="171.5" cy="20.5" r="6.5"/></g><g><g transform="translate(39.000000, 142.000000)"><ellipse cx="12.5" cy="12.5" fill="#FFF" stroke="#6B4FBB" stroke-width="4" rx="12.5" ry="12.5"/><path fill="#FC8A51" d="M10.7322 13.475l-1.7665-1.7667c-.5873-.5873-1.5368-.587-2.1226-.0012-.5897.59-.585 1.5362.0013 2.1226l2.826 2.826.0007.0007.0006.0006c.5898.5897 1.534.587 2.118.003l6.3704-6.3703c.577-.577.5826-1.5323-.003-2.118-.59-.59-1.5343-.5873-2.1183-.0033l-5.3065 5.3065z"/></g></g><circle cx="171.5" cy="122.5" r="6.5" fill="#FFF" stroke="#FC8A51" stroke-width="3"/><circle cx="22" cy="52" r="6" fill="#FFF" stroke="#B5A7DD" stroke-width="3"/><path fill="#FFF" stroke="#B5A7DD" stroke-width="3.6" d="M188.151 141.596c8.7045-7.7456 11.0126-20.9255 4.8625-31.5777-7.0208-12.1604-22.4055-16.422-34.363-9.5183-11.9572 6.9036-15.959 22.358-8.9382 34.5183 6.2353 10.8 19.068 15.3695 30.2375 11.4206l10.8992 18.8778c1.3167 2.2807 4.2302 3.063 6.5078 1.748 2.273-1.3122 3.0567-4.2295 1.74-6.51l-10.9458-18.9587zm-8.4343-4.6086c7.8576-4.5366 10.4874-14.6923 5.8738-22.6834-4.6137-7.991-14.7237-10.7915-22.5814-6.255-7.8575 4.5368-10.4873 14.6925-5.8737 22.6836 4.6137 7.991 14.7237 10.7915 22.5814 6.2548z"/></g></svg>
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 19221e3391f..8164f61797c 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -28,7 +28,7 @@
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
= link_to group, class: 'group-name' do
- = group.name
+ = group.full_name
- if group_member
as
diff --git a/app/views/shared/icons/_delta.svg b/app/views/shared/icons/_delta.svg
new file mode 100644
index 00000000000..7c0c0d3999c
--- /dev/null
+++ b/app/views/shared/icons/_delta.svg
@@ -0,0 +1,3 @@
+<svg width="14px" height="10px" viewBox="322 21 14 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="M330.078605,22.8166945 L335.259532,29.6235062 C335.615145,30.0907182 335.412062,30.4694683 334.822641,30.4694683 L331.657805,30.4694683 L324.04678,30.4694683 C323.449879,30.4694683 323.260751,30.0822112 323.609889,29.6235062 L328.790816,22.8166945 C329.146429,22.3494825 329.729467,22.3579895 330.078605,22.8166945 Z" id="delta" stroke="#5C5C5C" stroke-width="1" fill="none"></path>
+</svg>
diff --git a/app/views/shared/icons/_icon_cycle_analytics_overview.svg b/app/views/shared/icons/_icon_cycle_analytics_overview.svg
new file mode 100644
index 00000000000..eea9c975c35
--- /dev/null
+++ b/app/views/shared/icons/_icon_cycle_analytics_overview.svg
@@ -0,0 +1,81 @@
+<svg width="366px" height="229px" viewBox="784 258 366 229" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 41 (35326) - http://www.bohemiancoding.com/sketch -->
+ <desc>Created with Sketch.</desc>
+ <defs>
+ <rect id="path-1" x="35" y="39" width="24" height="21" rx="10"></rect>
+ <mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="24" height="21" fill="white">
+ <use xlink:href="#path-1"></use>
+ </mask>
+ <rect id="path-3" x="64.8662386" y="58.3882666" width="10" height="71" rx="5"></rect>
+ <mask id="mask-4" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="10" height="71" fill="white">
+ <use xlink:href="#path-3"></use>
+ </mask>
+ <rect id="path-5" x="18.1550472" y="58.3882666" width="10" height="71" rx="5"></rect>
+ <mask id="mask-6" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="10" height="71" fill="white">
+ <use xlink:href="#path-5"></use>
+ </mask>
+ <rect id="path-7" x="24" y="56" width="46" height="10" rx="5"></rect>
+ <mask id="mask-8" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="46" height="10" fill="white">
+ <use xlink:href="#path-7"></use>
+ </mask>
+ <rect id="path-9" x="42" y="60" width="10" height="68" rx="5"></rect>
+ <mask id="mask-10" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="10" height="68" fill="white">
+ <use xlink:href="#path-9"></use>
+ </mask>
+ <rect id="path-11" x="69" y="12" width="12" height="12" rx="3"></rect>
+ <mask id="mask-12" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="12" height="12" fill="white">
+ <use xlink:href="#path-11"></use>
+ </mask>
+ <rect id="path-13" x="40" y="18" width="14" height="22" rx="6"></rect>
+ <mask id="mask-14" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="14" height="22" fill="white">
+ <use xlink:href="#path-13"></use>
+ </mask>
+ <rect id="path-15" x="41" y="8" width="34" height="20" rx="3"></rect>
+ <mask id="mask-16" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="34" height="20" fill="white">
+ <use xlink:href="#path-15"></use>
+ </mask>
+ <path d="M8,8.00793008 C8,6.34669617 9.34984627,5.0321392 11.0036812,5.07151622 L46.9963188,5.92848378 C48.6552061,5.9679811 50,7.34177063 50,8.99109042 L50,27.0089096 C50,28.6608432 48.6501537,30.0321392 46.9963188,30.0715162 L11.0036812,30.9284838 C9.34479389,30.9679811 8,29.6568766 8,27.9920699 L8,8.00793008 Z" id="path-17"></path>
+ <mask id="mask-18" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="42" height="25.858699" fill="white">
+ <use xlink:href="#path-17"></use>
+ </mask>
+ <rect id="path-19" x="-7.10542736e-15" y="1.77635684e-14" width="16" height="36" rx="3"></rect>
+ <mask id="mask-20" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="16" height="36" fill="white">
+ <use xlink:href="#path-19"></use>
+ </mask>
+ </defs>
+ <g id="Group-7" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(786.000000, 259.000000)">
+ <g id="Group-5" transform="translate(132.727922, 71.000000)">
+ <use id="Rectangle-21" stroke="#EEEEEE" mask="url(#mask-2)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-1"></use>
+ <use id="Rectangle-16-Copy" stroke="#EEEEEE" mask="url(#mask-4)" stroke-width="8" fill="#FFFFFF" transform="translate(69.866239, 93.888267) rotate(-20.000000) translate(-69.866239, -93.888267) " xlink:href="#path-3"></use>
+ <use id="Rectangle-16-Copy-2" stroke="#EEEEEE" mask="url(#mask-6)" stroke-width="8" fill="#FFFFFF" transform="translate(23.155047, 93.888267) scale(-1, 1) rotate(-20.000000) translate(-23.155047, -93.888267) " xlink:href="#path-5"></use>
+ <use id="Rectangle-15" stroke="#EEEEEE" mask="url(#mask-8)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-7"></use>
+ <use id="Rectangle-16" stroke="#EEEEEE" mask="url(#mask-10)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-9"></use>
+ <g id="Group" transform="translate(45.500000, 33.000000) rotate(20.000000) translate(-45.500000, -33.000000) translate(5.000000, 13.000000)">
+ <use id="Rectangle-4" stroke="#EEEEEE" mask="url(#mask-12)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-11"></use>
+ <use id="Rectangle-20" stroke="#EEEEEE" mask="url(#mask-14)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-13"></use>
+ <use id="Rectangle-2" stroke="#EEEEEE" mask="url(#mask-16)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-15"></use>
+ <use id="Rectangle" stroke="#EEEEEE" mask="url(#mask-18)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-17"></use>
+ <rect id="Rectangle-17" fill="#EEEEEE" x="21" y="7" width="3" height="22"></rect>
+ <rect id="Rectangle-17-Copy" fill="#EEEEEE" x="64" y="8" width="3" height="17"></rect>
+ <circle id="Oval-9" fill="#B5A7DD" cx="40" cy="18" r="2"></circle>
+ <circle id="Oval-9-Copy-4" fill="#EEEEEE" cx="47" cy="33" r="2"></circle>
+ <use id="Rectangle-19" stroke="#EEEEEE" mask="url(#mask-20)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-19"></use>
+ </g>
+ </g>
+ <path d="M265.128496,225.286991 C247.289192,194.617726 214.068171,174 176.031622,174 C137.847583,174 104.51649,194.77793 86.7279221,225.644211" id="Oval-10" stroke="#EEEEEE" stroke-width="4" stroke-linecap="round" fill="#FFFFFF"></path>
+ <circle id="Oval-11" stroke="#FDE5D8" stroke-width="4" fill="#FFFFFF" cx="24.5" cy="25.5" r="24.5"></circle>
+ <path d="M24,1.00292933 C24,0.449026756 24.4438648,0 25,0 C25.5522847,0 26,0.437881351 26,1.00292933 L26,5.99707067 C26,6.55097324 25.5561352,7 25,7 C24.4477153,7 24,6.56211865 24,5.99707067 L24,1.00292933 Z M48.46461,17.3244238 C48.9914026,17.1532585 49.5556142,17.4366422 49.7274694,17.9655581 C49.8981348,18.4908122 49.6200365,19.0519274 49.0826439,19.2265369 L44.3329333,20.7698114 C43.8061406,20.9409767 43.241929,20.6575931 43.0700738,20.1286771 C42.8994084,19.6034231 43.1775067,19.0423078 43.7148993,18.8676984 L48.46461,17.3244238 Z M40.5019265,45.6352697 C40.8275022,46.0833863 40.7323394,46.7075538 40.2824166,47.0344419 C39.8356088,47.3590667 39.2160194,47.2679737 38.8838925,46.8108402 L35.9484099,42.770495 C35.6228341,42.3223784 35.717997,41.6982109 36.1679198,41.3713229 C36.6147275,41.0466981 37.234317,41.1377911 37.5664439,41.5949245 L40.5019265,45.6352697 Z M11.1161075,46.8108402 C10.7905317,47.2589568 10.1675063,47.3613299 9.71758344,47.0344419 C9.27077569,46.709817 9.16594665,46.0924031 9.49807352,45.6352697 L12.4335561,41.5949245 C12.7591319,41.1468079 13.3821574,41.0444348 13.8320802,41.3713229 C14.278888,41.6959477 14.383717,42.3133616 14.0515901,42.770495 L11.1161075,46.8108402 Z M0.917356057,19.2265369 C0.390563404,19.0553716 0.100675355,18.4944741 0.272530576,17.9655581 C0.44319595,17.4403041 0.997997482,17.1498144 1.53539005,17.3244238 L6.28510071,18.8676984 C6.81189336,19.0388637 7.10178141,19.5997611 6.92992619,20.1286771 C6.75926082,20.6539311 6.20445928,20.9444208 5.66706672,20.7698114 L0.917356057,19.2265369 Z" id="Rectangle-23" fill="#FDE5D8"></path>
+ <rect id="Rectangle-18" fill="#FC6D26" x="24" y="14" width="3" height="12" rx="1.5"></rect>
+ <rect id="Rectangle-22" fill="#FC6D26" x="24" y="24" width="12" height="3" rx="1.5"></rect>
+ <circle id="Oval-11" fill="#6B4FBB" cx="25.5" cy="25.5" r="2.5"></circle>
+ <path d="M358.949747,6.87474747 L357.453009,7.20729654 C356.9128,7.32732164 356.570654,6.9935311 356.692198,6.44648557 L357.024747,4.94974747 L356.692198,3.45300937 C356.572173,2.91279997 356.905964,2.57065443 357.453009,2.69219839 L358.949747,3.02474747 L360.446486,2.69219839 C360.986695,2.5721733 361.328841,2.90596384 361.207297,3.45300937 L360.874747,4.94974747 L361.207297,6.44648557 C361.327322,6.98669496 360.993531,7.32884051 360.446486,7.20729654 L358.949747,6.87474747 Z" id="Star-Copy-5" fill="#6B4FBB" transform="translate(358.949747, 4.949747) rotate(-315.000000) translate(-358.949747, -4.949747) "></path>
+ <path d="M113.949747,32.8747475 L112.453009,33.2072965 C111.9128,33.3273216 111.570654,32.9935311 111.692198,32.4464856 L112.024747,30.9497475 L111.692198,29.4530094 C111.572173,28.9128 111.905964,28.5706544 112.453009,28.6921984 L113.949747,29.0247475 L115.446486,28.6921984 C115.986695,28.5721733 116.328841,28.9059638 116.207297,29.4530094 L115.874747,30.9497475 L116.207297,32.4464856 C116.327322,32.986695 115.993531,33.3288405 115.446486,33.2072965 L113.949747,32.8747475 Z" id="Star-Copy-7" fill="#B5A7DD" transform="translate(113.949747, 30.949747) rotate(-315.000000) translate(-113.949747, -30.949747) "></path>
+ <path d="M329.949747,211.874747 L328.453009,212.207297 C327.9128,212.327322 327.570654,211.993531 327.692198,211.446486 L328.024747,209.949747 L327.692198,208.453009 C327.572173,207.9128 327.905964,207.570654 328.453009,207.692198 L329.949747,208.024747 L331.446486,207.692198 C331.986695,207.572173 332.328841,207.905964 332.207297,208.453009 L331.874747,209.949747 L332.207297,211.446486 C332.327322,211.986695 331.993531,212.328841 331.446486,212.207297 L329.949747,211.874747 Z" id="Star-Copy-6" fill="#B5A7DD" opacity="0.5" transform="translate(329.949747, 209.949747) rotate(-315.000000) translate(-329.949747, -209.949747) "></path>
+ <path d="M265.363961,54.838961 L263.153969,55.3299826 C262.617155,55.4492534 262.280283,55.1035008 262.397939,54.5739526 L262.888961,52.363961 L262.397939,50.1539694 C262.278669,49.6171548 262.624421,49.2802831 263.153969,49.3979395 L265.363961,49.888961 L267.573953,49.3979395 C268.110767,49.2786686 268.447639,49.6244213 268.329983,50.1539694 L267.838961,52.363961 L268.329983,54.5739526 C268.449253,55.1107673 268.103501,55.4476389 267.573953,55.3299826 L265.363961,54.838961 Z" id="Star-Copy-9" fill="#FC6D26" transform="translate(265.363961, 52.363961) rotate(-315.000000) translate(-265.363961, -52.363961) "></path>
+ <path d="M56.363961,142.838961 L54.1539694,143.329983 C53.6171548,143.449253 53.2802831,143.103501 53.3979395,142.573953 L53.888961,140.363961 L53.3979395,138.153969 C53.2786686,137.617155 53.6244213,137.280283 54.1539694,137.397939 L56.363961,137.888961 L58.5739526,137.397939 C59.1107673,137.278669 59.4476389,137.624421 59.3299826,138.153969 L58.838961,140.363961 L59.3299826,142.573953 C59.4492534,143.110767 59.1035008,143.447639 58.5739526,143.329983 L56.363961,142.838961 Z" id="Star-Copy-8" fill="#6B4FBB" transform="translate(56.363961, 140.363961) rotate(-315.000000) translate(-56.363961, -140.363961) "></path>
+ <g id="Group-6" transform="translate(311.872633, 125.094458) rotate(-345.000000) translate(-311.872633, -125.094458) translate(290.872633, 115.094458)">
+ <circle id="Oval-12" stroke="#FDE5D8" stroke-width="4" fill="#FFFFFF" cx="21" cy="10" r="10"></circle>
+ <ellipse id="Oval-13" fill="#FDE5D8" cx="21" cy="10" rx="21" ry="2"></ellipse>
+ </g>
+ </g>
+</svg>
diff --git a/app/views/shared/icons/_icon_lock.svg b/app/views/shared/icons/_icon_lock.svg
new file mode 100644
index 00000000000..6ec671a76ed
--- /dev/null
+++ b/app/views/shared/icons/_icon_lock.svg
@@ -0,0 +1,25 @@
+<svg width="46px" height="54px" viewBox="227 0 46 54" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 41 (35326) - http://www.bohemiancoding.com/sketch -->
+ <desc>Created with Sketch.</desc>
+ <defs>
+ <rect id="path-1" x="0" y="20" width="46" height="34" rx="8"></rect>
+ <mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="46" height="34" fill="white">
+ <use xlink:href="#path-1"></use>
+ </mask>
+ <path d="M29,16 C29,8.2680135 22.7319865,2 15,2 C7.2680135,2 1,8.2680135 1,16" id="path-3"></path>
+ <mask id="mask-4" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="28" height="14" fill="white">
+ <use xlink:href="#path-3"></use>
+ </mask>
+ </defs>
+ <g id="locker" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(227.000000, 0.000000)">
+ <g id="Group-8">
+ <use id="Rectangle-14" stroke="#B5A7DD" mask="url(#mask-2)" stroke-width="6" xlink:href="#path-1"></use>
+ <g id="Group-7" transform="translate(8.000000, 0.000000)">
+ <use id="Oval-3" stroke="#B5A7DD" mask="url(#mask-4)" stroke-width="6" xlink:href="#path-3"></use>
+ <rect id="Rectangle-13" fill="#B5A7DD" x="1" y="16" width="3" height="6"></rect>
+ <rect id="Rectangle-13-Copy" fill="#B5A7DD" x="26" y="16" width="3" height="6"></rect>
+ </g>
+ <path d="M25,37.4648712 C26.1956027,36.7732524 27,35.4805647 27,34 C27,31.790861 25.209139,30 23,30 C20.790861,30 19,31.790861 19,34 C19,35.4805647 19.8043973,36.7732524 21,37.4648712 L21,41.0026083 C21,42.1041422 21.8954305,43 23,43 C24.1122704,43 25,42.1057373 25,41.0026083 L25,37.4648712 Z" id="Combined-Shape" fill="#6B4FBB"></path>
+ </g>
+ </g>
+</svg>
diff --git a/app/views/shared/icons/_icon_no_data.svg b/app/views/shared/icons/_icon_no_data.svg
new file mode 100644
index 00000000000..ced8653b88c
--- /dev/null
+++ b/app/views/shared/icons/_icon_no_data.svg
@@ -0,0 +1,27 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="211 0 78 36" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <circle id="a" cx="5" cy="31" r="5"/>
+ <mask id="e" width="10" height="10" x="0" y="0" fill="#fff">
+ <use xlink:href="#a"/>
+ </mask>
+ <circle id="b" cx="29" cy="14" r="5"/>
+ <mask id="f" width="10" height="10" x="0" y="0" fill="#fff">
+ <use xlink:href="#b"/>
+ </mask>
+ <circle id="c" cx="53" cy="24" r="5"/>
+ <mask id="g" width="10" height="10" x="0" y="0" fill="#fff">
+ <use xlink:href="#c"/>
+ </mask>
+ <circle id="d" cx="73" cy="5" r="5"/>
+ <mask id="h" width="10" height="10" x="0" y="0" fill="#fff">
+ <use xlink:href="#d"/>
+ </mask>
+ </defs>
+ <g fill="none" fill-rule="evenodd" transform="translate(211)">
+ <path stroke="#B5A7DD" stroke-width="2" d="M5 31l24-17 26 10L73 5" stroke-linecap="round" stroke-dasharray="3 6"/>
+ <use fill="#FFF" stroke="#6B4FBB" stroke-width="6" mask="url(#e)" xlink:href="#a"/>
+ <use fill="#FFF" stroke="#6B4FBB" stroke-width="6" mask="url(#f)" xlink:href="#b"/>
+ <use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#g)" xlink:href="#c"/>
+ <use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#h)" xlink:href="#d"/>
+ </g>
+</svg>
diff --git a/app/views/shared/icons/_icon_status_canceled.svg b/app/views/shared/icons/_icon_status_canceled.svg
index 1b2d0891244..bd5d04e1cd7 100644..100755
--- a/app/views/shared/icons/_icon_status_canceled.svg
+++ b/app/views/shared/icons/_icon_status_canceled.svg
@@ -1,6 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" class="ci-status-icon-canceled" viewBox="0 0 14 14">
- <g fill="#5C5C5C" fill-rule="evenodd">
- <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/>
- <rect width="8" height="2" x="3" y="6" transform="rotate(45 7 7)" rx=".5"/>
- </g>
-</svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_created.svg b/app/views/shared/icons/_icon_status_created.svg
index dca5d289767..326ad04e017 100644..100755
--- a/app/views/shared/icons/_icon_status_created.svg
+++ b/app/views/shared/icons/_icon_status_created.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" class="ci-status-icon-created" viewBox="0 0 14 14" enable-background="new 0 0 14 14"><path d="M12.5,7 C12.5,4 10,1.5 7,1.5 C4,1.5 1.5,4 1.5,7 C1.5,10 4,12.5 7,12.5 C10,12.5 12.5,10 12.5,7 L12.5,7 Z M0,7 C0,3.1 3.1,0 7,0 C10.9,0 14,3.1 14,7 C14,10.9 10.9,14 7,14 C3.1,14 0,10.9 0,7 L0,7 Z" /><circle cx="7" cy="7" r="3.25"/></svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_failed.svg b/app/views/shared/icons/_icon_status_failed.svg
index e56e0887416..64da5aa31fc 100644..100755
--- a/app/views/shared/icons/_icon_status_failed.svg
+++ b/app/views/shared/icons/_icon_status_failed.svg
@@ -1,6 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
- <g fill="#D22852" fill-rule="evenodd">
- <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/>
- <path d="M7.72916667,6.27083333 L7.72916667,4.28939247 C7.72916667,4.12531853 7.59703895,4 7.43405116,4 L6.56594884,4 C6.40541585,4 6.27083333,4.12956542 6.27083333,4.28939247 L6.27083333,6.27083333 L4.28939247,6.27083333 C4.12531853,6.27083333 4,6.40296105 4,6.56594884 L4,7.43405116 C4,7.59458415 4.12956542,7.72916667 4.28939247,7.72916667 L6.27083333,7.72916667 L6.27083333,9.71060753 C6.27083333,9.87468147 6.40296105,10 6.56594884,10 L7.43405116,10 C7.59458415,10 7.72916667,9.87043458 7.72916667,9.71060753 L7.72916667,7.72916667 L9.71060753,7.72916667 C9.87468147,7.72916667 10,7.59703895 10,7.43405116 L10,6.56594884 C10,6.40541585 9.87043458,6.27083333 9.71060753,6.27083333 L7.72916667,6.27083333 Z" transform="rotate(-45 7 7)"/>
- </g>
-</svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_manual.svg b/app/views/shared/icons/_icon_status_manual.svg
new file mode 100755
index 00000000000..c98839f51a9
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_manual.svg
@@ -0,0 +1 @@
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_pending.svg b/app/views/shared/icons/_icon_status_pending.svg
index 117f0367161..02d5da407e3 100644..100755
--- a/app/views/shared/icons/_icon_status_pending.svg
+++ b/app/views/shared/icons/_icon_status_pending.svg
@@ -1,6 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
- <g fill="#E75E40" fill-rule="evenodd">
- <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/>
- <path d="M4.69999981,5.30065012 C4.69999981,5.13460564 4.83842754,5 5.00354719,5 L5.89645243,5 C6.06409702,5 6.19999981,5.13308716 6.19999981,5.30065012 L6.19999981,8.69934988 C6.19999981,8.86539436 6.06157207,9 5.89645243,9 L5.00354719,9 C4.8359026,9 4.69999981,8.86691284 4.69999981,8.69934988 L4.69999981,5.30065012 Z M7.69999981,5.30065012 C7.69999981,5.13460564 7.83842754,5 8.00354719,5 L8.89645243,5 C9.06409702,5 9.19999981,5.13308716 9.19999981,5.30065012 L9.19999981,8.69934988 C9.19999981,8.86539436 9.06157207,9 8.89645243,9 L8.00354719,9 C7.8359026,9 7.69999981,8.86691284 7.69999981,8.69934988 L7.69999981,5.30065012 Z"/>
- </g>
-</svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_running.svg b/app/views/shared/icons/_icon_status_running.svg
index 920d7952eb5..532f4fee33c 100644..100755
--- a/app/views/shared/icons/_icon_status_running.svg
+++ b/app/views/shared/icons/_icon_status_running.svg
@@ -1,6 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
- <g fill="#2D9FD8" fill-rule="evenodd">
- <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/>
- <path d="M7,3 C9.209139,3 11,4.790861 11,7 C11,9.209139 9.209139,11 7,11 C5.65802855,11 4.47040669,10.3391508 3.74481446,9.32513253 L7,7 L7,3 L7,3 Z"/>
- </g>
-</svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_skipped.svg b/app/views/shared/icons/_icon_status_skipped.svg
index 014ca86b61b..1998dfef9ea 100644..100755
--- a/app/views/shared/icons/_icon_status_skipped.svg
+++ b/app/views/shared/icons/_icon_status_skipped.svg
@@ -1 +1 @@
-<svg width="20" height="20" class="ci-status-icon-skipped" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Group Copy 31</title><g fill="#5C5C5C" fill-rule="evenodd"><path d="M10 17.857c4.286 0 7.857-3.571 7.857-7.857S14.286 2.143 10 2.143 2.143 5.714 2.143 10 5.714 17.857 10 17.857M10 0c5.571 0 10 4.429 10 10s-4.429 10-10 10S0 15.571 0 10 4.429 0 10 0"/><path d="M10.986 11l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.644-2.644a1.505 1.505 0 0 0 0-2.126l-2.644-2.644a1 1 0 0 0-1.414 1.414L10.986 9H6.4a1 1 0 0 0 0 2h4.586z"/></g></svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7.69 7.7l-.905.905a.7.7 0 0 0 .99.99l1.85-1.85c.411-.412.411-1.078 0-1.49l-1.85-1.85a.7.7 0 0 0-.99.99l.905.905H4.48a.7.7 0 0 0 0 1.4h3.21z"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_success.svg b/app/views/shared/icons/_icon_status_success.svg
index 67b378b3571..eed5006bebe 100644..100755
--- a/app/views/shared/icons/_icon_status_success.svg
+++ b/app/views/shared/icons/_icon_status_success.svg
@@ -1,6 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
- <g fill="#31AF64" fill-rule="evenodd">
- <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/>
- <path d="M7.29166667,7.875 L5.54840803,7.875 C5.38293028,7.875 5.25,8.00712771 5.25,8.17011551 L5.25,9.03821782 C5.25,9.19875081 5.38360183,9.33333333 5.54840803,9.33333333 L8.24853534,9.33333333 C8.52035522,9.33333333 8.75,9.11228506 8.75,8.83960819 L8.75,8.46475969 L8.75,4.07392947 C8.75,3.92144267 8.61787229,3.79166667 8.45488449,3.79166667 L7.58678218,3.79166667 C7.42624919,3.79166667 7.29166667,3.91804003 7.29166667,4.07392947 L7.29166667,7.875 Z" transform="rotate(45 7 6.563)"/>
- </g>
-</svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_warning.svg b/app/views/shared/icons/_icon_status_warning.svg
index d0ad4bd65b1..cb785635b7e 100644..100755
--- a/app/views/shared/icons/_icon_status_warning.svg
+++ b/app/views/shared/icons/_icon_status_warning.svg
@@ -1,6 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
- <g fill="#FF8A24" fill-rule="evenodd">
- <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/>
- <path d="M6,3.49769878 C6,3.22282734 6.21403503,3 6.50468445,3 L7.49531555,3 C7.77404508,3 8,3.21484375 8,3.49769878 L8,7.50230122 C8,7.77717266 7.78596497,8 7.49531555,8 L6.50468445,8 C6.22595492,8 6,7.78515625 6,7.50230122 L6,3.49769878 Z M6,9.50468445 C6,9.22595492 6.21403503,9 6.50468445,9 L7.49531555,9 C7.77404508,9 8,9.21403503 8,9.50468445 L8,10.4953156 C8,10.7740451 7.78596497,11 7.49531555,11 L6.50468445,11 C6.22595492,11 6,10.785965 6,10.4953156 L6,9.50468445 Z"/>
- </g>
-</svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></svg>
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index ed93857e6d4..e3503981afe 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -29,9 +29,9 @@
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
- - if issuable_filters_present
+ - if issuable_filter_present?
.filter-item.inline.reset-filters
- %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search])} Reset filters
+ %a{href: page_filter_path(without: issuable_filter_params)} Reset filters
.pull-right
- if boards_page
@@ -40,9 +40,9 @@
- if can?(current_user, :admin_list, @project)
.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) } }
- Create new list
+ 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: "Create a new list" }
+ = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
- if can?(current_user, :admin_label, @project)
= render partial: "shared/issuable/label_page_create"
= dropdown_loading
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 8d976952781..bdb00bfa33c 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -1,3 +1,4 @@
+- form = local_assigns.fetch(:f)
- project = @target_project || @project
= form_errors(issuable)
@@ -10,91 +11,22 @@
and make sure your changes will not unintentionally remove theirs
.form-group
- = f.label :title, class: 'control-label'
+ = form.label :title, class: 'control-label'
= render 'shared/issuable/form/template_selector', issuable: issuable
+ = render 'shared/issuable/form/title', issuable: issuable, form: form
- %div{ class: issuable_templates(issuable).any? ? 'col-sm-7 col-lg-8' : 'col-sm-10' }
- = f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
- class: 'form-control pad', required: true
+= render 'shared/issuable/form/description', issuable: issuable, form: form
- - if issuable.is_a?(MergeRequest)
- %p.help-block
- .js-wip-explanation
- %a.js-toggle-wip{href: "", tabindex: -1}
- Remove the
- %code WIP:
- prefix from the title
- to allow this
- %strong Work In Progress
- merge request to be merged when it's ready.
- .js-no-wip-explanation
- %a.js-toggle-wip{href: "", tabindex: -1}
- Start the title with
- %code WIP:
- to prevent a
- %strong Work In Progress
- merge request from being merged before it's ready.
-
- - if can_add_template?(issuable)
- %p.help-block
- Add
- = link_to "description templates", help_page_path('user/project/description_templates'), tabindex: -1
- to help your contributors communicate effectively!
-
-.form-group.detail-page-description
- = f.label :description, 'Description', class: 'control-label'
- .col-sm-10
-
- = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
- = render 'projects/zen', f: f, attr: :description,
- classes: 'note-textarea',
- placeholder: "Write a comment or drag your files here...",
- supports_slash_commands: !issuable.persisted?
- = render 'projects/notes/hints', supports_slash_commands: !issuable.persisted?
- .clearfix
- .error-alert
-
-- if issuable.is_a?(Issue)
+- if issuable.respond_to?(:confidential)
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
- = f.label :confidential do
- = f.check_box :confidential
+ = form.label :confidential do
+ = form.check_box :confidential
This issue is confidential and should only be visible to team members with at least Reporter access.
-- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
- - has_due_date = issuable.has_attribute?(:due_date)
- %hr
- .row
- %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
- .form-group.issue-assignee
- = f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
- .col-sm-10{ class: ("col-lg-8" if has_due_date) }
- .issuable-form-select-holder
- - if issuable.assignee_id
- = f.hidden_field :assignee_id
- = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
- placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
- .form-group.issue-milestone
- = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
- .col-sm-10{ class: ("col-lg-8" if has_due_date) }
- .issuable-form-select-holder
- = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
- .form-group
- - has_labels = @labels && @labels.any?
- = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
- = f.hidden_field :label_ids, multiple: true, value: ''
- .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
- .issuable-form-select-holder
- = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label"
- - if has_due_date
- .col-lg-6
- .form-group
- = f.label :due_date, "Due date", class: "control-label"
- .col-sm-10
- .issuable-form-select-holder
- = f.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
+= render 'shared/issuable/form/metadata', issuable: issuable, form: form
- if issuable.can_move?(current_user)
%hr
@@ -108,37 +40,29 @@
title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' }
= icon('question-circle')
-- if issuable.is_a?(MergeRequest) && !issuable.closed_without_fork?
- %hr
- - if @merge_request.new_record?
- .form-group
- = f.label :source_branch, class: 'control-label'
- .col-sm-10
- .issuable-form-select-holder
- = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true })
+= render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form
+
+- if @merge_request_for_resolving_discussions
.form-group
- = f.label :target_branch, class: 'control-label'
- .col-sm-10
- .issuable-form-select-holder
- = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} })
- - if @merge_request.new_record?
- &nbsp;
- = link_to 'Change branches', mr_change_branches_path(@merge_request)
- - if @merge_request.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'
- = check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch?
- Remove source branch when merge request is accepted.
+ .col-sm-10.col-sm-offset-2
+ - if @merge_request_for_resolving_discussions.discussions_can_be_resolved_by?(current_user)
+ = icon('exclamation-triangle')
+ Creating this issue will mark all discussions in
+ = link_to @merge_request_for_resolving_discussions.to_reference, merge_request_path(@merge_request_for_resolving_discussions)
+ as resolved.
+ = hidden_field_tag 'merge_request_for_resolving_discussions', @merge_request_for_resolving_discussions.iid
+ - else
+ = icon('exclamation-triangle')
+ You can't automatically mark all discussions in
+ = link_to @merge_request_for_resolving_discussions.to_reference, merge_request_path(@merge_request_for_resolving_discussions)
+ as resolved. Ask someone with sufficient rights to resolve the them.
- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?)
.row-content-block{class: (is_footer ? "footer-block" : "middle-block")}
- if issuable.new_record?
- = f.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
+ = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
- else
- = f.submit 'Save changes', class: 'btn btn-save'
+ = form.submit 'Save changes', class: 'btn btn-save'
- if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project))
.inline.prepend-left-10
@@ -151,8 +75,8 @@
- else
.pull-right
- if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project)
- = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" },
+ = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" },
method: :delete, class: 'btn btn-danger btn-grouped'
= link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
-= f.hidden_field :lock_version
+= form.hidden_field :lock_version
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index 1d778bc88de..22b5a6aa11b 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -22,7 +22,7 @@
%button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data}
%span.dropdown-toggle-text{ class: ("is-default" if selected.nil? || selected.empty?) }
= multi_label_name(selected, "Labels")
- = icon('caret-down')
+ = icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create }
- if show_create && project && can?(current_user, :admin_label, project)
diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml
index 3bc57d3d2ac..bd66f39fa59 100644
--- a/app/views/shared/issuable/_label_page_create.html.haml
+++ b/app/views/shared/issuable/_label_page_create.html.haml
@@ -9,7 +9,7 @@
&nbsp
.dropdown-label-color-input
.dropdown-label-color-preview.js-dropdown-label-color-preview
- %input#new_label_color.default-dropdown-input{ type: "text" }
+ %input#new_label_color.default-dropdown-input{ type: "text", placeholder: "Assign custom color like #FF0000" }
.clearfix
%button.btn.btn-primary.pull-left.js-new-label-btn{ type: "button" }
Create
diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml
index c0dc63be2bf..a8f01026ca5 100644
--- a/app/views/shared/issuable/_label_page_default.html.haml
+++ b/app/views/shared/issuable/_label_page_default.html.haml
@@ -1,17 +1,15 @@
- title = local_assigns.fetch(:title, 'Assign labels')
- show_create = local_assigns.fetch(:show_create, true)
- show_footer = local_assigns.fetch(:show_footer, true)
-- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels')
+- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search')
- show_boards_content = local_assigns.fetch(:show_boards_content, false)
.dropdown-page-one
= dropdown_title(title)
- if show_boards_content
.issue-board-dropdown-content
%p
- Each label that exists in your issue tracker can have its own dedicated
- list. Select a label below to add a list to your Board and it will
- automatically be populated with issues that have that label. To create
- a list for a label that doesn't exist yet, simply create the label below.
+ Create lists from the labels you use in your project. Issues with that
+ label will automatically be added to the list.
= dropdown_filter(filter_placeholder)
= dropdown_content
- if @project && show_footer
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index 5527a2f889a..d938edf4dbd 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -3,23 +3,23 @@
- issuables = @issues || @merge_requests
%ul.nav-links.issues-state-filters
- %li{class: ("active" if params[:state] == 'opened')}
- = link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do
+ %li{class: ("active" if params[:state] == 'opened')}>
+ = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened." do
#{issuables_state_counter_text(type, :opened)}
- if type == :merge_requests
- %li{class: ("active" if params[:state] == 'merged')}
- = link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do
+ %li{class: ("active" if params[:state] == 'merged')}>
+ = link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.' do
#{issuables_state_counter_text(type, :merged)}
- %li{class: ("active" if params[:state] == 'closed')}
- = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do
+ %li{class: ("active" if params[:state] == 'closed')}>
+ = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.' do
#{issuables_state_counter_text(type, :closed)}
- else
- %li{class: ("active" if params[:state] == 'closed')}
- = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do
+ %li{class: ("active" if params[:state] == 'closed')}>
+ = link_to page_filter_path(state: 'closed', label: true), id: 'state-all', title: 'Filter by issues that are currently closed.' do
#{issuables_state_counter_text(type, :closed)}
- %li{class: ("active" if params[:state] == 'all')}
- = link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do
+ %li{class: ("active" if params[:state] == 'all')}>
+ = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}." do
#{issuables_state_counter_text(type, :all)}
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 7363ead09ff..9fe1be5a597 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -9,12 +9,12 @@
%a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", aria: { label: "Toggle sidebar" } }
= sidebar_gutter_toggle_icon
- if current_user
- %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add Todo" : "Mark Done") }, data: { todo_text: "Add Todo", mark_text: "Mark Done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } }
+ %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add todo" : "Mark done") }, data: { todo_text: "Add todo", mark_text: "Mark done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } }
%span.js-issuable-todo-text
- if todo
- Mark Done
+ Mark done
- else
- Add Todo
+ Add todo
= icon('spin spinner', class: 'hidden js-issuable-todo-loading')
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
@@ -140,20 +140,15 @@
= render "shared/issuable/participants", participants: issuable.participants(current_user)
- if current_user
- - subscribed = issuable.subscribed?(current_user)
+ - subscribed = issuable.subscribed?(current_user, @project)
.block.light.subscription{data: {url: toggle_subscription_path(issuable)}}
.sidebar-collapsed-icon
= icon('rss')
- .title.hide-collapsed
+ %span.issuable-header-text.hide-collapsed.pull-left
Notifications
- subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
- %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
+ %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
%span= subscribed ? 'Unsubscribe' : 'Subscribe'
- .subscription-status.hide-collapsed{data: {status: subscribtion_status}}
- .unsubscribed{class: ( 'hidden' if subscribed )}
- You're not receiving notifications from this thread.
- .subscribed{class: ( 'hidden' unless subscribed )}
- You're receiving notifications because you're subscribed to this thread.
- project_ref = cross_project_reference(@project, issuable)
.block.project-reference
@@ -170,6 +165,6 @@
new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}');
new LabelsSelect();
new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
- new Subscription('.subscription')
+ gl.Subscription.bindAll('.subscription');
new gl.DueDateSelectors();
- sidebar = new Sidebar();
+ window.sidebar = new Sidebar();
diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml
new file mode 100644
index 00000000000..b757893ea04
--- /dev/null
+++ b/app/views/shared/issuable/form/_branch_chooser.html.haml
@@ -0,0 +1,30 @@
+- issuable = local_assigns.fetch(:issuable)
+- form = local_assigns.fetch(:form)
+
+- return unless issuable.is_a?(MergeRequest)
+- return if issuable.closed_without_fork?
+
+%hr
+- if issuable.new_record?
+ .form-group
+ = form.label :source_branch, class: 'control-label'
+ .col-sm-10
+ .issuable-form-select-holder
+ = form.select(:source_branch, [issuable.source_branch], {}, { class: 'source_branch select2 span2', disabled: true })
+.form-group
+ = form.label :target_branch, class: 'control-label'
+ .col-sm-10
+ .issuable-form-select-holder
+ = form.select(:target_branch, issuable.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: issuable.new_record?, data: { placeholder: "Select branch" }})
+ - 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/_description.html.haml b/app/views/shared/issuable/form/_description.html.haml
new file mode 100644
index 00000000000..dbace9ce401
--- /dev/null
+++ b/app/views/shared/issuable/form/_description.html.haml
@@ -0,0 +1,15 @@
+- issuable = local_assigns.fetch(:issuable)
+- form = local_assigns.fetch(:form)
+
+.form-group.detail-page-description
+ = form.label :description, 'Description', class: 'control-label'
+ .col-sm-10
+
+ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
+ = render 'projects/zen', f: form, attr: :description,
+ classes: 'note-textarea',
+ placeholder: "Write a comment or drag your files here...",
+ supports_slash_commands: !issuable.persisted?
+ = render 'projects/notes/hints', supports_slash_commands: !issuable.persisted?
+ .clearfix
+ .error-alert
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
new file mode 100644
index 00000000000..a47085230b8
--- /dev/null
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -0,0 +1,38 @@
+- issuable = local_assigns.fetch(:issuable)
+
+- return unless can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
+
+- has_due_date = issuable.has_attribute?(:due_date)
+- has_labels = @labels && @labels.any?
+- form = local_assigns.fetch(:form)
+
+%hr
+.row
+ %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
+ .form-group.issue-assignee
+ = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
+ .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+ .issuable-form-select-holder
+ - if issuable.assignee_id
+ = form.hidden_field :assignee_id
+ = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
+ placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
+ .form-group.issue-milestone
+ = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
+ .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+ .issuable-form-select-holder
+ = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
+ .form-group
+ - has_labels = @labels && @labels.any?
+ = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
+ = form.hidden_field :label_ids, multiple: true, value: ''
+ .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
+ .issuable-form-select-holder
+ = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label"
+ - if has_due_date
+ .col-lg-6
+ .form-group
+ = form.label :due_date, "Due date", class: "control-label"
+ .col-sm-10
+ .issuable-form-select-holder
+ = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml
new file mode 100644
index 00000000000..83efdc7c8f7
--- /dev/null
+++ b/app/views/shared/issuable/form/_title.html.haml
@@ -0,0 +1,32 @@
+- issuable = local_assigns.fetch(:issuable)
+- form = local_assigns.fetch(:form)
+- no_issuable_templates = issuable_templates(issuable).empty?
+- div_class = no_issuable_templates ? 'col-sm-10' : 'col-sm-7 col-lg-8'
+
+%div{ class: div_class }
+ = form.text_field :title, required: true, maxlength: 255, autofocus: true,
+ autocomplete: 'off', class: 'form-control pad'
+
+ - if issuable.respond_to?(:work_in_progress?)
+ %p.help-block
+ .js-wip-explanation
+ %a.js-toggle-wip{ href: '', tabindex: -1 }
+ Remove the
+ %code WIP:
+ prefix from the title
+ to allow this
+ %strong Work In Progress
+ merge request to be merged when it's ready.
+ .js-no-wip-explanation
+ %a.js-toggle-wip{ href: '', tabindex: -1 }
+ Start the title with
+ %code WIP:
+ to prevent a
+ %strong Work In Progress
+ merge request from being merged before it's ready.
+
+ - if no_issuable_templates && can?(current_user, :push_code, issuable.project)
+ %p.help-block
+ Add
+ = link_to 'description templates', help_page_path('user/project/description_templates'), tabindex: -1
+ to help your contributors communicate effectively!
diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml
index eff914398bb..e166dfab710 100644
--- a/app/views/shared/members/_access_request_buttons.html.haml
+++ b/app/views/shared/members/_access_request_buttons.html.haml
@@ -1,10 +1,16 @@
-- if can?(current_user, :request_access, source)
- - if requester = source.requesters.find_by(user_id: current_user.id)
- = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
- method: :delete,
- data: { confirm: remove_member_message(requester) },
- class: 'btn'
- - else
- = link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
- method: :post,
- class: 'btn'
+- model_name = source.model_name.to_s.downcase
+
+- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id))
+ = link_to "Leave #{model_name}", polymorphic_path([:leave, source, :members]),
+ method: :delete,
+ data: { confirm: leave_confirmation_message(source) },
+ class: 'btn'
+- elsif requester = source.requesters.find_by(user_id: current_user.id)
+ = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
+ method: :delete,
+ data: { confirm: remove_member_message(requester) },
+ class: 'btn'
+- elsif source.request_access_enabled && can?(current_user, :request_access, source)
+ = link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
+ method: :post,
+ class: 'btn'
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index 1c0346bbc78..8928de9097b 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -1,7 +1,8 @@
- group_link = local_assigns[:group_link]
- group = group_link.group
- can_admin_member = can?(current_user, :admin_project_member, @project)
-%li.member.group_member{ id: "group_member_#{group_link.id}" }
+- dom_id = "group_member_#{group_link.id}"
+%li.member.group_member{ id: dom_id }
%span{ class: "list-item-name" }
= image_tag group_icon(group), class: "avatar s40", alt: ''
%strong
@@ -14,7 +15,23 @@
Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
.controls.member-controls
= form_tag namespace_project_group_link_path(@project.namespace, @project, group_link), method: :put, remote: true, class: 'form-horizontal js-edit-member-form' do
- = select_tag 'group_link[group_access]', options_for_select(ProjectGroupLink.access_options, group_link.group_access), class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{group.id}", disabled: !can_admin_member
+ = hidden_field_tag "group_link[group_access]", group_link.group_access
+ .member-form-control.dropdown.append-right-5
+ %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
+ disabled: !can_admin_member,
+ data: { toggle: "dropdown", field_name: "group_link[group_access]" } }
+ %span.dropdown-toggle-text
+ = group_link.human_access
+ = icon("chevron-down")
+ .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable
+ = dropdown_title("Change permissions")
+ .dropdown-content
+ %ul
+ - Gitlab::Access.options.each do |role, role_id|
+ %li
+ = link_to role, "javascript:void(0)",
+ class: ("is-active" if group_link.group_access == role_id),
+ data: { id: role_id, el_id: dom_id }
.prepend-left-5.clearable-input.member-form-control
= text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{group.id}", disabled: !can_admin_member
%i.clear-icon.js-clear-input
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 432047a1c4e..659d4c905fc 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -20,8 +20,8 @@
%strong Blocked
- if source.instance_of?(Group) && !@group
- = link_to source, class: "member-group-link prepend-left-5" do
- = "· #{source.name}"
+ &middot;
+ = link_to source.name, source, class: "member-group-link"
.hidden-xs.cgray
- if member.request?
@@ -45,12 +45,28 @@
= time_ago_with_tooltip(member.created_at)
- if show_roles
.controls.member-controls
- - if show_controls
+ - if show_controls && (member.respond_to?(:group) && @group) || (member.respond_to?(:project) && @project)
- if user != current_user
= form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
- = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{member.id}", disabled: !can_admin_member
+ = f.hidden_field :access_level
+ .member-form-control.dropdown.append-right-5
+ %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
+ disabled: !can_admin_member,
+ data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]" } }
+ %span.dropdown-toggle-text
+ = member.human_access
+ = icon("chevron-down")
+ .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable
+ = dropdown_title("Change permissions")
+ .dropdown-content
+ %ul
+ - Gitlab::Access.options.each do |role, role_id|
+ %li
+ = link_to role, "javascript:void(0)",
+ class: ("is-active" if member.access_level == role_id),
+ data: { id: role_id, el_id: dom_id(member) }
.prepend-left-5.clearable-input.member-form-control
- = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member
+ = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member, data: { el_id: dom_id(member) }
%i.clear-icon.js-clear-input
- else
%span.member-access-text= member.human_access
diff --git a/app/views/shared/members/_sort_dropdown.html.haml b/app/views/shared/members/_sort_dropdown.html.haml
new file mode 100644
index 00000000000..bad0891f9f2
--- /dev/null
+++ b/app/views/shared/members/_sort_dropdown.html.haml
@@ -0,0 +1,9 @@
+.dropdown.inline.member-sort-dropdown
+ = dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' })
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %li.dropdown-header
+ Sort by
+ - member_sort_options_hash.each do |value, title|
+ %li
+ = link_to filter_group_project_member_path(sort: value), class: ("is-active" if @sort == value) do
+ = title
diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml
new file mode 100644
index 00000000000..748b10a1298
--- /dev/null
+++ b/app/views/shared/milestones/_form_dates.html.haml
@@ -0,0 +1,15 @@
+.col-md-6
+ .form-group
+ = f.label :start_date, "Start Date", class: "control-label"
+ .col-sm-10
+ = f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date"
+ %a.inline.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date
+.col-md-6
+ .form-group
+ = f.label :due_date, "Due Date", class: "control-label"
+ .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/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml
index dee2472fa79..d27fba805a3 100644
--- a/app/views/shared/milestones/_summary.html.haml
+++ b/app/views/shared/milestones/_summary.html.haml
@@ -3,32 +3,40 @@
.context.prepend-top-default
.milestone-summary
%h4 Progress
- %strong= milestone.issues_visible_to_user(current_user).size
- issues:
- %span.milestone-stat
- %strong= milestone.issues_visible_to_user(current_user).opened.size
- open and
- %strong= milestone.issues_visible_to_user(current_user).closed.size
- closed
- %strong= milestone.merge_requests.size
- merge requests:
- %span.milestone-stat
- %strong= milestone.merge_requests.opened.size
- open and
- %strong= milestone.merge_requests.merged.size
- merged
- %span.milestone-stat
- %strong== #{milestone.percent_complete(current_user)}%
- complete
- %span.milestone-stat
- %span.remaining-days= milestone_remaining_days(milestone)
- %span.pull-right.tab-issues-buttons
- - if project && can?(current_user, :create_issue, project)
- = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn btn-grouped", title: "New Issue" do
- New Issue
- = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn btn-grouped"
- %span.pull-right.tab-merge-requests-buttons.hidden
- = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn btn-grouped"
+ .milestone-stats-and-buttons
+ .milestone-stats
+ %span.milestone-stat.with-drilldown
+ %strong= milestone.issues_visible_to_user(current_user).size
+ issues:
+ %span.milestone-stat
+ %strong= milestone.issues_visible_to_user(current_user).opened.size
+ open and
+ %strong= milestone.issues_visible_to_user(current_user).closed.size
+ closed
+ %span.milestone-stat.with-drilldown
+ %strong= milestone.merge_requests.size
+ merge requests:
+ %span.milestone-stat
+ %strong= milestone.merge_requests.opened.size
+ open and
+ %strong= milestone.merge_requests.merged.size
+ merged
+ %span.milestone-stat
+ %strong== #{milestone.percent_complete(current_user)}%
+ complete
+ - remaining_days = milestone_remaining_days(milestone)
+ - if remaining_days.present?
+ %span.milestone-stat
+ %span.remaining-days= remaining_days
- = milestone_progress_bar(milestone)
+ .milestone-progress-buttons
+ %span.tab-issues-buttons
+ - if project && can?(current_user, :create_issue, project)
+ = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn", title: "New Issue" do
+ New Issue
+ = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn"
+ %span.tab-merge-requests-buttons.hidden
+ = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn"
+
+ = milestone_progress_bar(milestone)
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 548215243db..497446c1ef3 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -12,10 +12,10 @@
Open
%span.identifier
Milestone #{milestone.title}
- - if milestone.expires_at
+ - if milestone.due_date || milestone.start_date
%span.creator
&middot;
- = milestone.expires_at
+ = milestone_date_range(milestone)
- if group
.pull-right
- if can?(current_user, :admin_milestones, group)
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index feaa5570c21..fbad0d05de3 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -1,6 +1,5 @@
-- left_align = local_assigns[:left_align]
- if notification_setting
- .dropdown.notification-dropdown.pull-right
+ .dropdown.notification-dropdown
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
= hidden_setting_source_input(notification_setting)
= f.hidden_field :level, class: "notification_setting_level"
@@ -19,7 +18,7 @@
= notification_title(notification_setting.level)
= icon("caret-down")
- = render "shared/notifications/notification_dropdown", notification_setting: notification_setting, left_align: left_align
+ = render "shared/notifications/notification_dropdown", notification_setting: notification_setting
= content_for :scripts_body do
= render "shared/notifications/custom_notifications", notification_setting: notification_setting
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
index b704981e3db..a82fc95df84 100644
--- a/app/views/shared/notifications/_custom_notifications.html.haml
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -27,5 +27,5 @@
%label{ for: field_id }
= check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.events[event])
%strong
- = event.to_s.humanize
+ = notification_event_name(event)
= icon("spinner spin", class: "custom-notification-event-loading")
diff --git a/app/views/shared/notifications/_notification_dropdown.html.haml b/app/views/shared/notifications/_notification_dropdown.html.haml
index d3258ee64cb..85ad74f9a39 100644
--- a/app/views/shared/notifications/_notification_dropdown.html.haml
+++ b/app/views/shared/notifications/_notification_dropdown.html.haml
@@ -1,5 +1,4 @@
-- left_align = local_assigns[:left_align]
-%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting), ("dropdown-menu-align-right" unless left_align)] }
+%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting)] }
- NotificationSetting.levels.each_key do |level|
- next if level == "custom"
- next if level == "global" && notification_setting.source.nil?
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index d7506e07ff6..d084f5e9684 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -8,10 +8,6 @@
%span.creator
authored
= time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
- - if @snippet.updated_at != @snippet.created_at
- %span
- = icon('edit', title: 'edited')
- = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago')
by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")}
.snippet-actions
@@ -20,5 +16,9 @@
- else
= render "snippets/actions"
-%h2.snippet-title.prepend-top-0.append-bottom-0
- = markdown_field(@snippet, :title)
+.snippet-header
+ %h2.snippet-title.prepend-top-0.append-bottom-0
+ = markdown_field(@snippet, :title)
+
+ - if @snippet.updated_at != @snippet.created_at
+ = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago')
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index ea17bec8677..5d2d2317f22 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -1,17 +1,16 @@
+- link_project = local_assigns.fetch(:link_project, false)
+
%li.snippet-row
= image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: ''
.title
= link_to reliable_snippet_path(snippet) do
= snippet.title
- - if snippet.private?
- %span.label.label-gray.hidden-xs
- = icon('lock')
- private
- %span.monospace.pull-right.hidden-xs
- = snippet.file_name
+ - if snippet.file_name
+ %span.snippet-filename.monospace.hidden-xs
+ = snippet.file_name
- %ul.controls.visible-xs
+ %ul.controls
%li
- note_count = snippet.notes.user.count
= link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
@@ -22,11 +21,17 @@
= visibility_level_label(snippet.visibility_level)
= visibility_level_icon(snippet.visibility_level, fw: false)
- %small.pull-right.cgray.hidden-xs
- - if snippet.project_id?
- = link_to snippet.project.name_with_namespace, namespace_project_path(snippet.project.namespace, snippet.project)
-
- .snippet-info.hidden-xs
+ .snippet-info
+ #{snippet.to_reference} &middot;
+ authored #{time_ago_with_tooltip(snippet.created_at, placement: 'bottom', html_class: 'snippet-created-ago')}
+ by
= link_to user_snippets_path(snippet.author) do
= snippet.author_name
- authored #{time_ago_with_tooltip(snippet.created_at)}
+ - if link_project && snippet.project_id?
+ %span.hidden-xs
+ in
+ = link_to namespace_project_path(snippet.project.namespace, snippet.project) do
+ = snippet.project.name_with_namespace
+
+ .pull-right.snippet-updated-at
+ %span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')}
diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml
new file mode 100644
index 00000000000..5074afb63a1
--- /dev/null
+++ b/app/views/shared/tokens/_scopes_form.html.haml
@@ -0,0 +1,9 @@
+- scopes = local_assigns.fetch(:scopes)
+- prefix = local_assigns.fetch(:prefix)
+- token = local_assigns.fetch(:token)
+
+- scopes.each do |scope|
+ %fieldset
+ = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}"
+ = label_tag "#{prefix}_scopes_#{scope}", scope
+ %span= "(#{t(scope, scope: [:doorkeeper, :scopes])})"
diff --git a/app/views/shared/tokens/_scopes_list.html.haml b/app/views/shared/tokens/_scopes_list.html.haml
new file mode 100644
index 00000000000..f99e905e95c
--- /dev/null
+++ b/app/views/shared/tokens/_scopes_list.html.haml
@@ -0,0 +1,13 @@
+- token = local_assigns.fetch(:token)
+
+- return unless token.scopes.present?
+
+%tr
+ %td
+ Scopes
+ %td
+ %ul.scopes-list.append-bottom-0
+ - token.scopes.each do |scope|
+ %li
+ %span.scope-name= scope
+ = "(#{t(scope, scope: [:doorkeeper, :scopes])})"
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index 1d0e549ed3d..95fc7198104 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -1,13 +1,13 @@
.hidden-xs
- - if current_user
- = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New snippet" do
- New snippet
- - 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-danger", title: 'Delete Snippet' do
- Delete
- if can?(current_user, :update_personal_snippet, @snippet)
- = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do
+ = link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do
Edit
+ - 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" } }
diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml
index 77b66ca74b6..ac3701233ad 100644
--- a/app/views/snippets/_snippets.html.haml
+++ b/app/views/snippets/_snippets.html.haml
@@ -1,8 +1,9 @@
- remote = local_assigns.fetch(:remote, false)
+- link_project = local_assigns.fetch(:link_project, false)
.snippets-list-holder
%ul.content-list
- = render partial: 'shared/snippets/snippet', collection: @snippets
+ = render partial: 'shared/snippets/snippet', collection: @snippets, locals: { link_project: link_project }
- if @snippets.empty?
%li
.nothing-here-block Nothing here.
diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml
new file mode 100644
index 00000000000..2dda5fed647
--- /dev/null
+++ b/app/views/snippets/_snippets_scope_menu.html.haml
@@ -0,0 +1,31 @@
+- subject = local_assigns.fetch(:subject, current_user)
+- include_private = local_assigns.fetch(:include_private, false)
+
+.nav-links.snippet-scope-menu
+ %li{ class: ("active" unless params[:scope]) }
+ = link_to subject_snippets_path(subject) do
+ All
+ %span.badge
+ - if include_private
+ = subject.snippets.count
+ - else
+ = subject.snippets.public_and_internal.count
+
+ - if include_private
+ %li{ class: ("active" if params[:scope] == "are_private") }
+ = link_to subject_snippets_path(subject, scope: 'are_private') do
+ Private
+ %span.badge
+ = subject.snippets.are_private.count
+
+ %li{ class: ("active" if params[:scope] == "are_internal") }
+ = link_to subject_snippets_path(subject, scope: 'are_internal') do
+ Internal
+ %span.badge
+ = subject.snippets.are_internal.count
+
+ %li{ class: ("active" if params[:scope] == "are_public") }
+ = link_to subject_snippets_path(subject, scope: 'are_public') do
+ Public
+ %span.badge
+ = subject.snippets.are_public.count
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 27d7a6c5bb6..837a1a0cc8c 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -7,9 +7,9 @@
= blob_icon 0, @snippet.file_name
= @snippet.file_name
.file-actions
- = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
+ = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm")
= link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
= link_to 'Download', download_snippet_path(@snippet), class: "btn btn-sm"
= render 'shared/snippets/blob'
-= render 'award_emoji/awards_block', awardable: @snippet, inline: true \ No newline at end of file
+= render 'award_emoji/awards_block', awardable: @snippet, inline: true
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
new file mode 100644
index 00000000000..fccddb70d18
--- /dev/null
+++ b/app/workers/authorized_projects_worker.rb
@@ -0,0 +1,34 @@
+class AuthorizedProjectsWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ LEASE_TIMEOUT = 1.minute.to_i
+
+ def self.bulk_perform_async(args_list)
+ Sidekiq::Client.push_bulk('class' => self, 'args' => args_list)
+ end
+
+ def perform(user_id)
+ user = User.find_by(id: user_id)
+
+ refresh(user) if user
+ end
+
+ def refresh(user)
+ lease_key = "refresh_authorized_projects:#{user.id}"
+ lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
+
+ until uuid = lease.try_obtain
+ # Keep trying until we obtain the lease. If we don't do so we may end up
+ # not updating the list of authorized projects properly. To prevent
+ # hammering Redis too much we'll wait for a bit between retries.
+ sleep(1)
+ end
+
+ begin
+ user.refresh_authorized_projects
+ ensure
+ Gitlab::ExclusiveLease.cancel(lease_key, uuid)
+ end
+ end
+end
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index e0ad5268664..e17add7421f 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -4,15 +4,13 @@ class BuildSuccessWorker
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
- create_deployment(build)
+ create_deployment(build) if build.has_environment?
end
end
private
def create_deployment(build)
- return if build.environment.blank?
-
service = CreateDeploymentService.new(
build.project, build.user,
environment: build.environment,
diff --git a/app/workers/delete_merged_branches_worker.rb b/app/workers/delete_merged_branches_worker.rb
new file mode 100644
index 00000000000..f870da4ecfd
--- /dev/null
+++ b/app/workers/delete_merged_branches_worker.rb
@@ -0,0 +1,20 @@
+class DeleteMergedBranchesWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def perform(project_id, user_id)
+ begin
+ project = Project.find(project_id)
+ rescue ActiveRecord::RecordNotFound
+ return
+ end
+
+ user = User.find(user_id)
+
+ begin
+ DeleteMergedBranchesService.new(project, user).execute
+ rescue Gitlab::Access::AccessDeniedError
+ return
+ end
+ end
+end
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index c3e62bb88c0..926162b8c53 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -2,10 +2,14 @@ class NewNoteWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
- def perform(note_id, note_params)
- note = Note.find(note_id)
-
- NotificationService.new.new_note(note)
- Notes::PostProcessService.new(note).execute
+ # Keep extra parameter to preserve backwards compatibility with
+ # old `NewNoteWorker` jobs (can remove later)
+ def perform(note_id, _params = {})
+ if note = Note.find_by(id: note_id)
+ NotificationService.new.new_note(note)
+ Notes::PostProcessService.new(note).execute
+ else
+ Rails.logger.error("NewNoteWorker: couldn't find note with ID=#{note_id}, skipping job")
+ end
end
end
diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb
index 34f6ef161fb..070943f1ecc 100644
--- a/app/workers/pipeline_metrics_worker.rb
+++ b/app/workers/pipeline_metrics_worker.rb
@@ -12,11 +12,11 @@ class PipelineMetricsWorker
private
def update_metrics_for_active_pipeline(pipeline)
- metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil)
+ metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil, pipeline_id: pipeline.id)
end
def update_metrics_for_succeeded_pipeline(pipeline)
- metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: pipeline.finished_at)
+ metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: pipeline.finished_at, pipeline_id: pipeline.id)
end
def metrics(pipeline)
diff --git a/app/workers/pipeline_notification_worker.rb b/app/workers/pipeline_notification_worker.rb
new file mode 100644
index 00000000000..cdb860b6675
--- /dev/null
+++ b/app/workers/pipeline_notification_worker.rb
@@ -0,0 +1,12 @@
+class PipelineNotificationWorker
+ include Sidekiq::Worker
+ include PipelineQueue
+
+ def perform(pipeline_id, recipients = nil)
+ pipeline = Ci::Pipeline.find_by(id: pipeline_id)
+
+ return unless pipeline
+
+ NotificationService.new.pipeline_finished(pipeline, recipients)
+ end
+end
diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb
index 2aa6fff24da..cc0eb708cf9 100644
--- a/app/workers/pipeline_success_worker.rb
+++ b/app/workers/pipeline_success_worker.rb
@@ -4,7 +4,7 @@ class PipelineSuccessWorker
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
- MergeRequests::MergeWhenBuildSucceedsService
+ MergeRequests::MergeWhenPipelineSucceedsService
.new(pipeline.project, nil)
.trigger(pipeline)
end
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index 071741fbacd..e9a5bd7f24e 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -10,9 +10,10 @@ class ProcessCommitWorker
# project_id - The ID of the project this commit belongs to.
# user_id - The ID of the user that pushed the commit.
- # commit_sha - The SHA1 of the commit to process.
+ # commit_hash - Hash containing commit details to use for constructing a
+ # Commit object without having to use the Git repository.
# default - The data was pushed to the default branch.
- def perform(project_id, user_id, commit_sha, default = false)
+ def perform(project_id, user_id, commit_hash, default = false)
project = Project.find_by(id: project_id)
return unless project
@@ -21,10 +22,7 @@ class ProcessCommitWorker
return unless user
- commit = find_commit(project, commit_sha)
-
- return unless commit
-
+ commit = build_commit(project, commit_hash)
author = commit.author || user
process_commit_message(project, commit, user, author, default)
@@ -59,9 +57,18 @@ class ProcessCommitWorker
update_all(first_mentioned_in_commit_at: commit.committed_date)
end
- private
+ def build_commit(project, hash)
+ date_suffix = '_date'
+
+ # When processing Sidekiq payloads various timestamps are stored as Strings.
+ # Commit in turn expects Time-like instances upon input, so we have to
+ # manually parse these values.
+ hash.each do |key, value|
+ if key.to_s.end_with?(date_suffix) && value.is_a?(String)
+ hash[key] = Time.parse(value)
+ end
+ end
- def find_commit(project, sha)
- project.commit(sha)
+ Commit.from_hash(hash, project)
end
end
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index 4dfa745fb50..27d7e652721 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -1,54 +1,38 @@
# Worker for updating any project specific caches.
-#
-# This worker runs at most once every 15 minutes per project. This is to ensure
-# that multiple instances of jobs for this worker don't hammer the underlying
-# storage engine as much.
class ProjectCacheWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
LEASE_TIMEOUT = 15.minutes.to_i
- def self.lease_for(project_id)
- Gitlab::ExclusiveLease.
- new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT)
- end
+ # project_id - The ID of the project for which to flush the cache.
+ # refresh - An Array containing extra types of data to refresh such as
+ # `:readme` to flush the README and `:changelog` to flush the
+ # CHANGELOG.
+ def perform(project_id, refresh = [])
+ project = Project.find_by(id: project_id)
- # Overwrite Sidekiq's implementation so we only schedule when actually needed.
- def self.perform_async(project_id)
- # If a lease for this project is still being held there's no point in
- # scheduling a new job.
- super unless lease_for(project_id).exists?
- end
+ return unless project && project.repository.exists?
- def perform(project_id)
- if try_obtain_lease_for(project_id)
- Rails.logger.
- info("Obtained ProjectCacheWorker lease for project #{project_id}")
- else
- Rails.logger.
- info("Could not obtain ProjectCacheWorker lease for project #{project_id}")
-
- return
- end
+ update_repository_size(project)
+ project.update_commit_count
- update_caches(project_id)
+ project.repository.refresh_method_caches(refresh.map(&:to_sym))
end
- def update_caches(project_id)
- project = Project.find(project_id)
+ def update_repository_size(project)
+ return unless try_obtain_lease_for(project.id, :update_repository_size)
- return unless project.repository.exists?
+ Rails.logger.info("Updating repository size for project #{project.id}")
project.update_repository_size
- project.update_commit_count
-
- if project.repository.root_ref
- project.repository.build_cache
- end
end
- def try_obtain_lease_for(project_id)
- self.class.lease_for(project_id).try_obtain
+ private
+
+ def try_obtain_lease_for(project_id, section)
+ Gitlab::ExclusiveLease.
+ new("project_cache_worker:#{project_id}:#{section}", timeout: LEASE_TIMEOUT).
+ try_obtain
end
end
diff --git a/bin/changelog b/bin/changelog
index b6586ebb6aa..e07b1ad237a 100755
--- a/bin/changelog
+++ b/bin/changelog
@@ -158,7 +158,7 @@ class ChangelogEntry
end
def branch_name
- @branch_name ||= %x{git symbolic-ref HEAD}.strip.sub(%r{\Arefs/heads/}, '')
+ @branch_name ||= %x{git symbolic-ref --short HEAD}.strip
end
end
diff --git a/bin/rspec-stackprof b/bin/rspec-stackprof
new file mode 100755
index 00000000000..df79feb201d
--- /dev/null
+++ b/bin/rspec-stackprof
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+
+require 'stackprof'
+$:.unshift 'spec'
+require 'rails_helper'
+
+filename = ARGV[0].split('/').last
+interval = ENV.fetch('INTERVAL', 1000).to_i
+limit = ENV.fetch('LIMIT', 20)
+output_file = "tmp/#{filename}.dump"
+
+StackProf.run(mode: :wall, out: output_file, interval: interval) do
+ RSpec::Core::Runner.run(ARGV, $stderr, $stdout)
+end
+
+system("stackprof #{output_file} --text --limit #{limit}")
diff --git a/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml b/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml
new file mode 100644
index 00000000000..19c76b5b437
--- /dev/null
+++ b/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml
@@ -0,0 +1,4 @@
+---
+title: Fix wrong tab selected when loggin fails and multiple login tabs exists
+merge_request: 7314
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/18435-autocomplete-is-not-performant.yml b/changelogs/unreleased/18435-autocomplete-is-not-performant.yml
new file mode 100644
index 00000000000..019c55e27dc
--- /dev/null
+++ b/changelogs/unreleased/18435-autocomplete-is-not-performant.yml
@@ -0,0 +1,4 @@
+---
+title: Made comment autocomplete more performant and removed some loading bugs
+merge_request: 6856
+author:
diff --git a/changelogs/unreleased/18546-update-wiki-page-design.yml b/changelogs/unreleased/18546-update-wiki-page-design.yml
new file mode 100644
index 00000000000..c76e17340f2
--- /dev/null
+++ b/changelogs/unreleased/18546-update-wiki-page-design.yml
@@ -0,0 +1,4 @@
+---
+title: Update wiki page design
+merge_request: 7429
+author:
diff --git a/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml b/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml
new file mode 100644
index 00000000000..742b10e72aa
--- /dev/null
+++ b/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml
@@ -0,0 +1,4 @@
+---
+title: group authors in contribution graph with case insensitive email handle comparison
+merge_request: 8021
+author:
diff --git a/changelogs/unreleased/20052-actions-table-vscroll.yml b/changelogs/unreleased/20052-actions-table-vscroll.yml
new file mode 100644
index 00000000000..779cd08de09
--- /dev/null
+++ b/changelogs/unreleased/20052-actions-table-vscroll.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent overflow with vertical scroll when we have space to show content
+merge_request: 8061
+author:
diff --git a/changelogs/unreleased/20492-access-token-scopes.yml b/changelogs/unreleased/20492-access-token-scopes.yml
new file mode 100644
index 00000000000..a9424ded662
--- /dev/null
+++ b/changelogs/unreleased/20492-access-token-scopes.yml
@@ -0,0 +1,4 @@
+---
+title: Add scopes for personal access tokens and OAuth tokens
+merge_request: 5951
+author:
diff --git a/changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml b/changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml
deleted file mode 100644
index 8f03746ff80..00000000000
--- a/changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add setting to only allow merge requests to be merged when all discussions are resolved
-merge_request: 7125
-author: Rodolfo Arruda
diff --git a/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml b/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml
deleted file mode 100644
index 95d8fef1099..00000000000
--- a/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use the Gitlab Workhorse HTTP header in the admin dashboard
-merge_request:
-author: Chris Wright
diff --git a/changelogs/unreleased/22373-reduce-queries-in-api-helpers-find_project.yml b/changelogs/unreleased/22373-reduce-queries-in-api-helpers-find_project.yml
new file mode 100644
index 00000000000..7f1d40e7c21
--- /dev/null
+++ b/changelogs/unreleased/22373-reduce-queries-in-api-helpers-find_project.yml
@@ -0,0 +1,4 @@
+---
+title: 'Make API::Helpers find a project with only one query'
+merge_request: 7714
+author:
diff --git a/changelogs/unreleased/22588-todos-filter-shows-all-users.yml b/changelogs/unreleased/22588-todos-filter-shows-all-users.yml
deleted file mode 100644
index 1da72142880..00000000000
--- a/changelogs/unreleased/22588-todos-filter-shows-all-users.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'Fix: Todos Filter Shows All Users'
-merge_request:
-author:
diff --git a/changelogs/unreleased/22604-manual-actions.yml b/changelogs/unreleased/22604-manual-actions.yml
new file mode 100644
index 00000000000..7335e597292
--- /dev/null
+++ b/changelogs/unreleased/22604-manual-actions.yml
@@ -0,0 +1,4 @@
+---
+title: Resolve "Manual actions on pipeline graph"
+merge_request: 7931
+author:
diff --git a/changelogs/unreleased/22719-provide-a-new-gitlab-workhorse-install-rake-task-similar-to-gitlab-shell-install.yml b/changelogs/unreleased/22719-provide-a-new-gitlab-workhorse-install-rake-task-similar-to-gitlab-shell-install.yml
new file mode 100644
index 00000000000..54bd313f075
--- /dev/null
+++ b/changelogs/unreleased/22719-provide-a-new-gitlab-workhorse-install-rake-task-similar-to-gitlab-shell-install.yml
@@ -0,0 +1,4 @@
+---
+title: New `gitlab:workhorse:install` rake task
+merge_request: 6574
+author:
diff --git a/changelogs/unreleased/22781-user-generated-permalinks.yml b/changelogs/unreleased/22781-user-generated-permalinks.yml
new file mode 100644
index 00000000000..e46739e48e3
--- /dev/null
+++ b/changelogs/unreleased/22781-user-generated-permalinks.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent DOM ID collisions resulting from user-generated content anchors
+merge_request: 7631
+author:
diff --git a/changelogs/unreleased/22849-ci-build-ref-slug.yml b/changelogs/unreleased/22849-ci-build-ref-slug.yml
new file mode 100644
index 00000000000..b159ecca6d8
--- /dev/null
+++ b/changelogs/unreleased/22849-ci-build-ref-slug.yml
@@ -0,0 +1,4 @@
+---
+title: Introduce $CI_BUILD_REF_SLUG
+merge_request: 8072
+author:
diff --git a/changelogs/unreleased/22864-add-environment-slug.yml b/changelogs/unreleased/22864-add-environment-slug.yml
new file mode 100644
index 00000000000..f90f79337d5
--- /dev/null
+++ b/changelogs/unreleased/22864-add-environment-slug.yml
@@ -0,0 +1,4 @@
+---
+title: Add a slug to environments
+merge_request: 7983
+author:
diff --git a/changelogs/unreleased/22864-kubernetes-service.yml b/changelogs/unreleased/22864-kubernetes-service.yml
new file mode 100644
index 00000000000..ea1323cbeb0
--- /dev/null
+++ b/changelogs/unreleased/22864-kubernetes-service.yml
@@ -0,0 +1,4 @@
+---
+title: Introduce deployment services, starting with a KubernetesService
+merge_request: 7994
+author:
diff --git a/changelogs/unreleased/22947-fix_issues_atom_feed_url.yml b/changelogs/unreleased/22947-fix_issues_atom_feed_url.yml
deleted file mode 100644
index 2312afdb3d7..00000000000
--- a/changelogs/unreleased/22947-fix_issues_atom_feed_url.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Issues atom feed url reflect filters on dashboard
-merge_request: 7114
-author: Lucas Deschamps
diff --git a/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml b/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml
deleted file mode 100644
index 7b54d3df56d..00000000000
--- a/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Rewrite git blame spinach feature tests to rspec feature tests
-merge_request: 7197
-author: Lisanne Fellinger
diff --git a/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml b/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml
new file mode 100644
index 00000000000..99dbe4a32a0
--- /dev/null
+++ b/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml
@@ -0,0 +1,5 @@
+---
+title: Moved Leave Project and Leave Group buttons to access_request_buttons from
+ the settings dropdown
+merge_request: 7600
+author:
diff --git a/changelogs/unreleased/23500-enable-colorvariable.yml b/changelogs/unreleased/23500-enable-colorvariable.yml
new file mode 100644
index 00000000000..98e22a934b8
--- /dev/null
+++ b/changelogs/unreleased/23500-enable-colorvariable.yml
@@ -0,0 +1,4 @@
+---
+title: Enable ColorVariable in scss-lint
+merge_request:
+author: Sam Rose
diff --git a/changelogs/unreleased/23532-define-common-helper-for-describe-pagination-params-in-api.yml b/changelogs/unreleased/23532-define-common-helper-for-describe-pagination-params-in-api.yml
new file mode 100644
index 00000000000..bb9e96d7581
--- /dev/null
+++ b/changelogs/unreleased/23532-define-common-helper-for-describe-pagination-params-in-api.yml
@@ -0,0 +1,4 @@
+---
+title: Define common helper for describe pagination params in api
+merge_request: 7646
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/23573-sort-functionality-for-project-member.yml b/changelogs/unreleased/23573-sort-functionality-for-project-member.yml
new file mode 100644
index 00000000000..73de0a6351b
--- /dev/null
+++ b/changelogs/unreleased/23573-sort-functionality-for-project-member.yml
@@ -0,0 +1,4 @@
+---
+title: Add sorting functionality for group/project members
+merge_request: 7032
+author:
diff --git a/changelogs/unreleased/23589-open-issue-for-mr.yml b/changelogs/unreleased/23589-open-issue-for-mr.yml
new file mode 100644
index 00000000000..cea48b85254
--- /dev/null
+++ b/changelogs/unreleased/23589-open-issue-for-mr.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve all discussions in a merge request by creating an issue collecting
+ them
+merge_request: 7180
+author: Bob Van Landuyt
diff --git a/changelogs/unreleased/23718-backup-rake-task-human-readable.yml b/changelogs/unreleased/23718-backup-rake-task-human-readable.yml
new file mode 100644
index 00000000000..2e7583244ac
--- /dev/null
+++ b/changelogs/unreleased/23718-backup-rake-task-human-readable.yml
@@ -0,0 +1,4 @@
+---
+title: Add Human Readable format for rake backup
+merge_request: 7188
+author: David Gerő
diff --git a/changelogs/unreleased/23961-can-t-share-project-with-groups.yml b/changelogs/unreleased/23961-can-t-share-project-with-groups.yml
deleted file mode 100644
index b3bfcbda4b7..00000000000
--- a/changelogs/unreleased/23961-can-t-share-project-with-groups.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Only skip group when it's actually a group in the "Share with group" select
-merge_request: 7262
-author:
diff --git a/changelogs/unreleased/24056-guest-sees-some-project-details-and-gets-404.yml b/changelogs/unreleased/24056-guest-sees-some-project-details-and-gets-404.yml
deleted file mode 100644
index 8ca0c5beab3..00000000000
--- a/changelogs/unreleased/24056-guest-sees-some-project-details-and-gets-404.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'Fix: Guest sees some repository details and gets 404'
-merge_request:
-author:
diff --git a/changelogs/unreleased/24059-round-robin-repository-storage.yml b/changelogs/unreleased/24059-round-robin-repository-storage.yml
deleted file mode 100644
index 109536114ff..00000000000
--- a/changelogs/unreleased/24059-round-robin-repository-storage.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Introduce round-robin project creation to spread load over multiple shards
-merge_request: 7266
-author:
diff --git a/changelogs/unreleased/24102-cannot-unselect-remove-source-branch-when-editing-merge-request.yml b/changelogs/unreleased/24102-cannot-unselect-remove-source-branch-when-editing-merge-request.yml
deleted file mode 100644
index 50d018170f1..00000000000
--- a/changelogs/unreleased/24102-cannot-unselect-remove-source-branch-when-editing-merge-request.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Ensure merge request's "remove branch" accessors return booleans
-merge_request: 7267
-author:
diff --git a/changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml b/changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml
new file mode 100644
index 00000000000..855e4e1ba1d
--- /dev/null
+++ b/changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml
@@ -0,0 +1,4 @@
+---
+title: Moved new projects button below new group button on the welcome screen
+merge_request: 7770
+author:
diff --git a/changelogs/unreleased/24150-consistent-dropdown-styles.yml b/changelogs/unreleased/24150-consistent-dropdown-styles.yml
new file mode 100644
index 00000000000..a328d796c43
--- /dev/null
+++ b/changelogs/unreleased/24150-consistent-dropdown-styles.yml
@@ -0,0 +1,4 @@
+---
+title: Homogenize filter and sort dropdown look'n'feel
+merge_request: 7583
+author: David Wagner
diff --git a/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml b/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml
new file mode 100644
index 00000000000..2227c81bd34
--- /dev/null
+++ b/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml
@@ -0,0 +1,4 @@
+---
+title: Remove the help text under the sidebar subscribe button and style it inline
+merge_request: 7389
+author:
diff --git a/changelogs/unreleased/24413-show-unconfirmed-email-status.yml b/changelogs/unreleased/24413-show-unconfirmed-email-status.yml
new file mode 100644
index 00000000000..972eaed95e0
--- /dev/null
+++ b/changelogs/unreleased/24413-show-unconfirmed-email-status.yml
@@ -0,0 +1,4 @@
+---
+title: Shows unconfirmed email status in profile
+merge_request: 7611
+author:
diff --git a/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml b/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml
new file mode 100644
index 00000000000..34999480d4a
--- /dev/null
+++ b/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml
@@ -0,0 +1,4 @@
+---
+title: 'Remove unnecessary target branch link from MR page in case of deleted target branch'
+merge_request: 7916
+author: Rydkin Maxim
diff --git a/changelogs/unreleased/24576_cant_stop_impersonating.yml b/changelogs/unreleased/24576_cant_stop_impersonating.yml
new file mode 100644
index 00000000000..8fa6eeca756
--- /dev/null
+++ b/changelogs/unreleased/24576_cant_stop_impersonating.yml
@@ -0,0 +1,4 @@
+---
+title: Allow admins to stop impersonating users without e-mail addresses
+merge_request: 7550
+author: Oren Kanner
diff --git a/changelogs/unreleased/24710-fix-generic-commit-status-table-row.yml b/changelogs/unreleased/24710-fix-generic-commit-status-table-row.yml
new file mode 100644
index 00000000000..07cb53d5278
--- /dev/null
+++ b/changelogs/unreleased/24710-fix-generic-commit-status-table-row.yml
@@ -0,0 +1,4 @@
+---
+title: Update generic/external build status to match normal build status template
+merge_request: 7811
+author:
diff --git a/changelogs/unreleased/24726-remove-across-gitlab.yml b/changelogs/unreleased/24726-remove-across-gitlab.yml
new file mode 100644
index 00000000000..6436e4b688f
--- /dev/null
+++ b/changelogs/unreleased/24726-remove-across-gitlab.yml
@@ -0,0 +1,4 @@
+---
+title: 24726 Remove Across GitLab from side navigation
+merge_request:
+author:
diff --git a/changelogs/unreleased/24733-archived-project-merge-request-count.yml b/changelogs/unreleased/24733-archived-project-merge-request-count.yml
new file mode 100644
index 00000000000..2bc7e91825a
--- /dev/null
+++ b/changelogs/unreleased/24733-archived-project-merge-request-count.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Archived project merge requests add to group's Merge Requests
+merge_request: 7790
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml b/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml
new file mode 100644
index 00000000000..b9d84c0ce31
--- /dev/null
+++ b/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml
@@ -0,0 +1,5 @@
+---
+title: Changed cursor icon to pointer when mousing over stages on the Cycle Analytics
+ pages
+merge_request:
+author: Ryan Harris
diff --git a/changelogs/unreleased/24807-stop-ddosing-ourselves.yml b/changelogs/unreleased/24807-stop-ddosing-ourselves.yml
new file mode 100644
index 00000000000..49e6c5e56e5
--- /dev/null
+++ b/changelogs/unreleased/24807-stop-ddosing-ourselves.yml
@@ -0,0 +1,4 @@
+---
+title: Use SmartInterval for MR widget and improve visibilitychange functionality
+merge_request: 7762
+author:
diff --git a/changelogs/unreleased/24824-dropdown-items-focus.yml b/changelogs/unreleased/24824-dropdown-items-focus.yml
new file mode 100644
index 00000000000..66970c2a9a5
--- /dev/null
+++ b/changelogs/unreleased/24824-dropdown-items-focus.yml
@@ -0,0 +1,4 @@
+---
+title: Add focus state to dropdown items
+merge_request:
+author:
diff --git a/changelogs/unreleased/24844-environments-date.yml b/changelogs/unreleased/24844-environments-date.yml
new file mode 100644
index 00000000000..2bc23d40a68
--- /dev/null
+++ b/changelogs/unreleased/24844-environments-date.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes Environments displaying incorrect date since 8.14 upgrade
+merge_request:
+author:
diff --git a/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml b/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml
new file mode 100644
index 00000000000..cc7c2604824
--- /dev/null
+++ b/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml
@@ -0,0 +1,4 @@
+---
+title: Improve bulk assignment for issuables
+merge_request:
+author:
diff --git a/changelogs/unreleased/24921-hide-prompt-to-add-ssh-key-if-ssh-protocol-is-disabled.yml b/changelogs/unreleased/24921-hide-prompt-to-add-ssh-key-if-ssh-protocol-is-disabled.yml
new file mode 100644
index 00000000000..4d4019e770e
--- /dev/null
+++ b/changelogs/unreleased/24921-hide-prompt-to-add-ssh-key-if-ssh-protocol-is-disabled.yml
@@ -0,0 +1,4 @@
+---
+title: Don't display prompt to add SSH keys if SSH protocol is disabled
+merge_request: 7840
+author: Andrew Smith (EspadaV8)
diff --git a/changelogs/unreleased/24927-custom-event-polyfill-test.yml b/changelogs/unreleased/24927-custom-event-polyfill-test.yml
new file mode 100644
index 00000000000..879c28a951e
--- /dev/null
+++ b/changelogs/unreleased/24927-custom-event-polyfill-test.yml
@@ -0,0 +1,4 @@
+---
+title: Adds tests for custom event polyfill
+merge_request: 7996
+author:
diff --git a/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml b/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml
new file mode 100644
index 00000000000..b8ba9391530
--- /dev/null
+++ b/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml
@@ -0,0 +1,4 @@
+---
+title: Add image controls to MR diffs
+merge_request: 7919
+author:
diff --git a/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml b/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml
new file mode 100644
index 00000000000..12ea08e3815
--- /dev/null
+++ b/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml
@@ -0,0 +1,5 @@
+---
+title: 'fix: 24982- Remove''Signed in successfully'' message After this change the
+ sign-in-success flash message will not be shown'
+merge_request: 7837
+author: jnoortheen
diff --git a/changelogs/unreleased/24999-fix-project-avatar-alignment.yml b/changelogs/unreleased/24999-fix-project-avatar-alignment.yml
new file mode 100644
index 00000000000..7af812e7359
--- /dev/null
+++ b/changelogs/unreleased/24999-fix-project-avatar-alignment.yml
@@ -0,0 +1,4 @@
+---
+title: Adjust the width of project avatars to fix alignment within their container
+merge_request:
+author: Ryan Harris
diff --git a/changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml b/changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml
new file mode 100644
index 00000000000..cc8b0e28277
--- /dev/null
+++ b/changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml
@@ -0,0 +1,4 @@
+---
+title: Sentence cased the nav tab headers on the project dashboard page
+merge_request:
+author: Ryan Harris
diff --git a/changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml b/changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml
new file mode 100644
index 00000000000..2c3ba1dfe44
--- /dev/null
+++ b/changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml
@@ -0,0 +1,4 @@
+---
+title: Adds hoverstates for collapsed Issue/Merge Request sidebar
+merge_request: !7777
+author:
diff --git a/changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml b/changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml
new file mode 100644
index 00000000000..a7b5810f1bf
--- /dev/null
+++ b/changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml
@@ -0,0 +1,4 @@
+---
+title: Redirect to sign-in page when unauthenticated user tries to create a snippet
+merge_request: 7786
+author:
diff --git a/changelogs/unreleased/25031-do-not-raise-error-in-autocomplete.yml b/changelogs/unreleased/25031-do-not-raise-error-in-autocomplete.yml
new file mode 100644
index 00000000000..862de7c5db1
--- /dev/null
+++ b/changelogs/unreleased/25031-do-not-raise-error-in-autocomplete.yml
@@ -0,0 +1,4 @@
+---
+title: Do not raise error in AutocompleteController#users when not authorized
+merge_request: 7817
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/25098-header-margins-on-pipeline-settings.yml b/changelogs/unreleased/25098-header-margins-on-pipeline-settings.yml
new file mode 100644
index 00000000000..1799fad1631
--- /dev/null
+++ b/changelogs/unreleased/25098-header-margins-on-pipeline-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Adjusted margins for Build Status and Coverage Report rows to match those of
+ the CI/CD Pipeline row
+merge_request:
+author: Ryan Harris
diff --git a/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml b/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml
new file mode 100644
index 00000000000..62030d3fc45
--- /dev/null
+++ b/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent user creating issue or MR without signing in for a group
+merge_request: 7902
+author:
diff --git a/changelogs/unreleased/25136-last-deployment-link.yml b/changelogs/unreleased/25136-last-deployment-link.yml
new file mode 100644
index 00000000000..eab1534aa66
--- /dev/null
+++ b/changelogs/unreleased/25136-last-deployment-link.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Latest deployment link is broken
+merge_request: 7839
+author:
diff --git a/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml b/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml
new file mode 100644
index 00000000000..531b0f83099
--- /dev/null
+++ b/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml
@@ -0,0 +1,4 @@
+---
+title: Improve help message for issue create slash command
+merge_request: 7850
+author:
diff --git a/changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml b/changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml
new file mode 100644
index 00000000000..a7576e2cbdb
--- /dev/null
+++ b/changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml
@@ -0,0 +1,4 @@
+---
+title: Remove wrong '.builds-feature' class from the MR settings fieldset
+merge_request: 7930
+author:
diff --git a/changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml b/changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml
new file mode 100644
index 00000000000..7afc794866b
--- /dev/null
+++ b/changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml
@@ -0,0 +1,5 @@
+---
+title: Centered Accept Merge Request button within MR widget and added padding for
+ viewports smaller than 768px
+merge_request:
+author: Ryan Harris
diff --git a/changelogs/unreleased/25207-text-overflow-env-table.yml b/changelogs/unreleased/25207-text-overflow-env-table.yml
new file mode 100644
index 00000000000..69348281a50
--- /dev/null
+++ b/changelogs/unreleased/25207-text-overflow-env-table.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent enviroment table to overflow when name has underscores
+merge_request: 8142
+author:
diff --git a/changelogs/unreleased/25221-fix-build-status-overflow-mobile.yml b/changelogs/unreleased/25221-fix-build-status-overflow-mobile.yml
new file mode 100644
index 00000000000..52de34478f0
--- /dev/null
+++ b/changelogs/unreleased/25221-fix-build-status-overflow-mobile.yml
@@ -0,0 +1,4 @@
+---
+title: Added top margin to Build status page header for mobile views
+merge_request:
+author: Ryan Harris
diff --git a/changelogs/unreleased/25251-actionview-template-error-undefined-method-text-for-nil-nilclass.yml b/changelogs/unreleased/25251-actionview-template-error-undefined-method-text-for-nil-nilclass.yml
new file mode 100644
index 00000000000..7f1c417bc77
--- /dev/null
+++ b/changelogs/unreleased/25251-actionview-template-error-undefined-method-text-for-nil-nilclass.yml
@@ -0,0 +1,5 @@
+---
+title: 'Fixes "ActionView::Template::Error: undefined method `text?` for nil:NilClass"
+ on MR pages'
+merge_request:
+author:
diff --git a/changelogs/unreleased/25264-ref-commit.yml b/changelogs/unreleased/25264-ref-commit.yml
new file mode 100644
index 00000000000..13a33da9801
--- /dev/null
+++ b/changelogs/unreleased/25264-ref-commit.yml
@@ -0,0 +1,4 @@
+---
+title: Change ref property to commitRef in vue commit component
+merge_request: 7901
+author:
diff --git a/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml b/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml
new file mode 100644
index 00000000000..79cb2c6d843
--- /dev/null
+++ b/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml
@@ -0,0 +1,4 @@
+---
+title: 'Fix comments activity tab visibility condition'
+merge_request: 7913
+author: Rydkin Maxim
diff --git a/changelogs/unreleased/25294-remove-signed-out-msg.yml b/changelogs/unreleased/25294-remove-signed-out-msg.yml
new file mode 100644
index 00000000000..567294fe5f7
--- /dev/null
+++ b/changelogs/unreleased/25294-remove-signed-out-msg.yml
@@ -0,0 +1,4 @@
+---
+title: 'fix: removed signed_out notification'
+merge_request: 7958
+author: jnoortheen
diff --git a/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml b/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml
new file mode 100644
index 00000000000..0770f9752a0
--- /dev/null
+++ b/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml
@@ -0,0 +1,4 @@
+---
+title: Changed Housekeeping button on project settings page to default styling
+merge_request:
+author: Ryan Harris
diff --git a/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml b/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml
new file mode 100644
index 00000000000..b12eab26b67
--- /dev/null
+++ b/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml
@@ -0,0 +1,4 @@
+---
+title: Ensure issuable state changes only fire webhooks once
+merge_request:
+author:
diff --git a/changelogs/unreleased/25374-svg-as-prop.yml b/changelogs/unreleased/25374-svg-as-prop.yml
new file mode 100644
index 00000000000..45a71b55b3b
--- /dev/null
+++ b/changelogs/unreleased/25374-svg-as-prop.yml
@@ -0,0 +1,4 @@
+---
+title: Resolve "Provide SVG as a prop instead of hiding and copy them in environments table"
+merge_request: 7992
+author:
diff --git a/changelogs/unreleased/25482-fix-api-sudo.yml b/changelogs/unreleased/25482-fix-api-sudo.yml
new file mode 100644
index 00000000000..4c11fe1622e
--- /dev/null
+++ b/changelogs/unreleased/25482-fix-api-sudo.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Memoize the current_user so that sudo can work properly'
+merge_request: 8017
+author:
diff --git a/changelogs/unreleased/25483-broken-tabs.yml b/changelogs/unreleased/25483-broken-tabs.yml
new file mode 100644
index 00000000000..d6c92014bea
--- /dev/null
+++ b/changelogs/unreleased/25483-broken-tabs.yml
@@ -0,0 +1,4 @@
+---
+title: Fix TypeError: Cannot read property 'initTabs' on commit builds tab
+merge_request: 8009
+author:
diff --git a/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml b/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml
new file mode 100644
index 00000000000..c6a92547c5c
--- /dev/null
+++ b/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml
@@ -0,0 +1,4 @@
+---
+title: Added go back anchor on error pages.
+merge_request: 8087
+author:
diff --git a/changelogs/unreleased/25617-todos-filter-placeholder.yml b/changelogs/unreleased/25617-todos-filter-placeholder.yml
new file mode 100644
index 00000000000..5d0adb04ef3
--- /dev/null
+++ b/changelogs/unreleased/25617-todos-filter-placeholder.yml
@@ -0,0 +1,4 @@
+---
+title: 25617 Fix placeholder color of todo filters
+merge_request:
+author:
diff --git a/changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml b/changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml
new file mode 100644
index 00000000000..0a81124de0d
--- /dev/null
+++ b/changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml
@@ -0,0 +1,4 @@
+---
+title: fix colors and margins for adjacent alert banners
+merge_request: 8151
+author:
diff --git a/changelogs/unreleased/4269-public-api.yml b/changelogs/unreleased/4269-public-api.yml
new file mode 100644
index 00000000000..560bc6a4f13
--- /dev/null
+++ b/changelogs/unreleased/4269-public-api.yml
@@ -0,0 +1,4 @@
+---
+title: Allow public access to some Project API endpoints
+merge_request: 7843
+author:
diff --git a/changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml b/changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml
new file mode 100644
index 00000000000..9dd04d3f089
--- /dev/null
+++ b/changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml
@@ -0,0 +1,3 @@
+title: Add setting to enable/disable HTML emails
+merge_request: 7749
+author:
diff --git a/changelogs/unreleased/8003-katex-math.yml b/changelogs/unreleased/8003-katex-math.yml
new file mode 100644
index 00000000000..a40dcde1393
--- /dev/null
+++ b/changelogs/unreleased/8003-katex-math.yml
@@ -0,0 +1,4 @@
+---
+title: Added support for math rendering, using KaTeX, in Markdown and asciidoc
+merge_request: 8003
+author: Munken
diff --git a/changelogs/unreleased/abuse_report-fixture.yml b/changelogs/unreleased/abuse_report-fixture.yml
new file mode 100644
index 00000000000..47478a2048b
--- /dev/null
+++ b/changelogs/unreleased/abuse_report-fixture.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for abuse_reports_spec
+merge_request: 7644
+author: winniehell
diff --git a/changelogs/unreleased/add-api-label-id.yml b/changelogs/unreleased/add-api-label-id.yml
deleted file mode 100644
index 3af4f5e677d..00000000000
--- a/changelogs/unreleased/add-api-label-id.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Expose label IDs in API
-merge_request: 7275
-author: Rares Sfirlogea
diff --git a/changelogs/unreleased/add-project-import-data-index.yml b/changelogs/unreleased/add-project-import-data-index.yml
deleted file mode 100644
index f5e4005f544..00000000000
--- a/changelogs/unreleased/add-project-import-data-index.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add an index for project_id in project_import_data to improve performance
-merge_request:
-author:
diff --git a/changelogs/unreleased/add_info_to_qr.yml b/changelogs/unreleased/add_info_to_qr.yml
new file mode 100644
index 00000000000..a4b0354a9c9
--- /dev/null
+++ b/changelogs/unreleased/add_info_to_qr.yml
@@ -0,0 +1,4 @@
+---
+title: Add GitLab host to 2FA QR code and manual info
+merge_request: 6941
+author:
diff --git a/changelogs/unreleased/allow-more-filenames.yml b/changelogs/unreleased/allow-more-filenames.yml
new file mode 100644
index 00000000000..7989f94e528
--- /dev/null
+++ b/changelogs/unreleased/allow-more-filenames.yml
@@ -0,0 +1,4 @@
+---
+title: Allow all alphanumeric characters in file names
+merge_request: 8002
+author: winniehell
diff --git a/changelogs/unreleased/api-branch-status.yml b/changelogs/unreleased/api-branch-status.yml
new file mode 100644
index 00000000000..c5763345a22
--- /dev/null
+++ b/changelogs/unreleased/api-branch-status.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Expose merge status for branch API'
+merge_request:
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-cherry-pick.yml b/changelogs/unreleased/api-cherry-pick.yml
new file mode 100644
index 00000000000..5f4cee450b9
--- /dev/null
+++ b/changelogs/unreleased/api-cherry-pick.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Ability to cherry pick a commit'
+merge_request: 8047
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-delete-group-share.yml b/changelogs/unreleased/api-delete-group-share.yml
new file mode 100644
index 00000000000..26cfb35bba3
--- /dev/null
+++ b/changelogs/unreleased/api-delete-group-share.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Add ability to unshare a project from a group'
+merge_request: 7662
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-expose-commiter-details.yml b/changelogs/unreleased/api-expose-commiter-details.yml
new file mode 100644
index 00000000000..5ee34adc5c9
--- /dev/null
+++ b/changelogs/unreleased/api-expose-commiter-details.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Expose committer details for commits'
+merge_request:
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-label-priorities.yml b/changelogs/unreleased/api-label-priorities.yml
deleted file mode 100644
index 85b6c2761bb..00000000000
--- a/changelogs/unreleased/api-label-priorities.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: API: Ability to retrieve version information
-merge_request: 7286
-author: Robert Schilling
diff --git a/changelogs/unreleased/api-remove-source-branch.yml b/changelogs/unreleased/api-remove-source-branch.yml
new file mode 100644
index 00000000000..d1b6507aedb
--- /dev/null
+++ b/changelogs/unreleased/api-remove-source-branch.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Ability to set ''should_remove_source_branch'' on merge requests'
+merge_request:
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml b/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml
deleted file mode 100644
index d132d7e79c3..00000000000
--- a/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Return 400 when creating a system hook fails
-merge_request: 7350
-author: Robert Schilling
diff --git a/changelogs/unreleased/api-simple-group-project.yml b/changelogs/unreleased/api-simple-group-project.yml
new file mode 100644
index 00000000000..54c8de610a6
--- /dev/null
+++ b/changelogs/unreleased/api-simple-group-project.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Simple representation of group''s projects'
+merge_request: 8060
+author: Robert Schilling
diff --git a/changelogs/unreleased/awards_handler.yml b/changelogs/unreleased/awards_handler.yml
new file mode 100644
index 00000000000..1f9904c0691
--- /dev/null
+++ b/changelogs/unreleased/awards_handler.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for awards_handler_spec
+merge_request: 7661
+author: winniehell
diff --git a/changelogs/unreleased/bitbucket-oauth2.yml b/changelogs/unreleased/bitbucket-oauth2.yml
new file mode 100644
index 00000000000..97d82518b7b
--- /dev/null
+++ b/changelogs/unreleased/bitbucket-oauth2.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor Bitbucket importer to use BitBucket API Version 2
+merge_request:
+author:
diff --git a/changelogs/unreleased/broken-link-frontend-dev-guide.yml b/changelogs/unreleased/broken-link-frontend-dev-guide.yml
deleted file mode 100644
index d7b6f4a7701..00000000000
--- a/changelogs/unreleased/broken-link-frontend-dev-guide.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix broken link to observatory cli on Frontend Dev Guide
-merge_request:
-author: Sam Rose
diff --git a/changelogs/unreleased/change_development_build_fixtures.yml b/changelogs/unreleased/change_development_build_fixtures.yml
new file mode 100644
index 00000000000..b5dc3792745
--- /dev/null
+++ b/changelogs/unreleased/change_development_build_fixtures.yml
@@ -0,0 +1,4 @@
+---
+title: Ci::Builds have same ref as Ci::Pipeline in dev fixtures
+merge_request:
+author: twonegatives
diff --git a/changelogs/unreleased/chomp-git-status-message.yml b/changelogs/unreleased/chomp-git-status-message.yml
new file mode 100644
index 00000000000..f70607df7a1
--- /dev/null
+++ b/changelogs/unreleased/chomp-git-status-message.yml
@@ -0,0 +1,5 @@
+---
+title: For single line git commit messages, the close quote should be on the same
+ line as the open quote
+merge_request:
+author:
diff --git a/changelogs/unreleased/cleanup-common_utils-js.yml b/changelogs/unreleased/cleanup-common_utils-js.yml
new file mode 100644
index 00000000000..54d81b76c28
--- /dev/null
+++ b/changelogs/unreleased/cleanup-common_utils-js.yml
@@ -0,0 +1,4 @@
+---
+title: Clean up common_utils.js
+merge_request: 7318
+author: winniehell
diff --git a/changelogs/unreleased/comments-fixture.yml b/changelogs/unreleased/comments-fixture.yml
new file mode 100644
index 00000000000..824c1c88a60
--- /dev/null
+++ b/changelogs/unreleased/comments-fixture.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for notes_spec
+merge_request: 7683
+author: winniehell
diff --git a/changelogs/unreleased/create-dynamic-fixture-for-build_spec.yml b/changelogs/unreleased/create-dynamic-fixture-for-build_spec.yml
new file mode 100644
index 00000000000..f0d9ff0c34f
--- /dev/null
+++ b/changelogs/unreleased/create-dynamic-fixture-for-build_spec.yml
@@ -0,0 +1,4 @@
+---
+title: Create dynamic fixture for build_spec
+merge_request: 7589
+author: winniehell
diff --git a/changelogs/unreleased/destroy-session.yml b/changelogs/unreleased/destroy-session.yml
new file mode 100644
index 00000000000..e713e2dc424
--- /dev/null
+++ b/changelogs/unreleased/destroy-session.yml
@@ -0,0 +1,4 @@
+---
+title: Destroy a user's session when they delete their own account
+merge_request:
+author:
diff --git a/changelogs/unreleased/dev-issue-24554.yml b/changelogs/unreleased/dev-issue-24554.yml
new file mode 100644
index 00000000000..0bb362b9325
--- /dev/null
+++ b/changelogs/unreleased/dev-issue-24554.yml
@@ -0,0 +1,4 @@
+---
+title: Edit help text to clarify annotated tag creation.
+merge_request:
+author: Liz Lam
diff --git a/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml b/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml
new file mode 100644
index 00000000000..12b1460f388
--- /dev/null
+++ b/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml
@@ -0,0 +1,4 @@
+---
+title: Do not reload diff for merge request made from fork when target branch in fork is updated
+merge_request: 7973
+author:
diff --git a/changelogs/unreleased/dz-allow-nested-group-routing.yml b/changelogs/unreleased/dz-allow-nested-group-routing.yml
new file mode 100644
index 00000000000..9d8e6e17914
--- /dev/null
+++ b/changelogs/unreleased/dz-allow-nested-group-routing.yml
@@ -0,0 +1,4 @@
+---
+title: Add nested groups support to the routing
+merge_request: 7459
+author:
diff --git a/changelogs/unreleased/dz-nested-groups.yml b/changelogs/unreleased/dz-nested-groups.yml
new file mode 100644
index 00000000000..c227c5a8ea5
--- /dev/null
+++ b/changelogs/unreleased/dz-nested-groups.yml
@@ -0,0 +1,4 @@
+---
+title: Add nested groups support on data level
+merge_request:
+author:
diff --git a/changelogs/unreleased/emoji-btn-disabled.yml b/changelogs/unreleased/emoji-btn-disabled.yml
new file mode 100644
index 00000000000..a18b553d513
--- /dev/null
+++ b/changelogs/unreleased/emoji-btn-disabled.yml
@@ -0,0 +1,4 @@
+---
+title: Disabled emoji buttons when user is not logged in
+merge_request:
+author:
diff --git a/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml b/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml
new file mode 100644
index 00000000000..9c52e53c3b4
--- /dev/null
+++ b/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml
@@ -0,0 +1,4 @@
+---
+title: Enable AsciiDoctor admonition icons
+merge_request: 7812
+author: Horacio Sanson
diff --git a/changelogs/unreleased/expose-deployment-variables.yml b/changelogs/unreleased/expose-deployment-variables.yml
new file mode 100644
index 00000000000..7663d5b6ae5
--- /dev/null
+++ b/changelogs/unreleased/expose-deployment-variables.yml
@@ -0,0 +1,4 @@
+---
+title: Pass variables from deployment project services to CI runner
+merge_request: 8107
+author:
diff --git a/changelogs/unreleased/feature-admin-user-groups-link.yml b/changelogs/unreleased/feature-admin-user-groups-link.yml
new file mode 100644
index 00000000000..b89c08f82d7
--- /dev/null
+++ b/changelogs/unreleased/feature-admin-user-groups-link.yml
@@ -0,0 +1,4 @@
+---
+title: The admin user projects view now has a clickable group link
+merge_request: 7620
+author: James Gregory
diff --git a/changelogs/unreleased/features-api-snippets.yml b/changelogs/unreleased/features-api-snippets.yml
new file mode 100644
index 00000000000..80c7bb75359
--- /dev/null
+++ b/changelogs/unreleased/features-api-snippets.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Endpoint to expose personal snippets as /snippets'
+merge_request: 6373
+author: Bernard Guyzmo Pratz
diff --git a/changelogs/unreleased/file-template-dropwdown-proper-position.yml b/changelogs/unreleased/file-template-dropwdown-proper-position.yml
new file mode 100644
index 00000000000..cf2a622b7e6
--- /dev/null
+++ b/changelogs/unreleased/file-template-dropwdown-proper-position.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed file template dropdown for the "New File" editor for smaller/zoomed screens
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml b/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml
deleted file mode 100644
index d1bc8ea2eb1..00000000000
--- a/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix 404 on network page when entering non-existent git revision
-merge_request: 7172
-author: Hiroyuki Sato
diff --git a/changelogs/unreleased/fix-cancelling-pipelines.yml b/changelogs/unreleased/fix-cancelling-pipelines.yml
new file mode 100644
index 00000000000..c21e663093a
--- /dev/null
+++ b/changelogs/unreleased/fix-cancelling-pipelines.yml
@@ -0,0 +1,4 @@
+---
+title: Fix cancelling created or external pipelines
+merge_request: 7508
+author:
diff --git a/changelogs/unreleased/fix-create-pipeline-with-builds-in-transaction.yml b/changelogs/unreleased/fix-create-pipeline-with-builds-in-transaction.yml
new file mode 100644
index 00000000000..e37841e80c3
--- /dev/null
+++ b/changelogs/unreleased/fix-create-pipeline-with-builds-in-transaction.yml
@@ -0,0 +1,4 @@
+---
+title: Create builds in transaction to avoid empty pipelines
+merge_request: 7742
+author:
diff --git a/changelogs/unreleased/fix-drop-project-authorized-for-user.yml b/changelogs/unreleased/fix-drop-project-authorized-for-user.yml
new file mode 100644
index 00000000000..0d11969575a
--- /dev/null
+++ b/changelogs/unreleased/fix-drop-project-authorized-for-user.yml
@@ -0,0 +1,4 @@
+---
+title: Use authorized projects in ProjectTeam
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-invalid-filename-eslint.yml b/changelogs/unreleased/fix-invalid-filename-eslint.yml
deleted file mode 100644
index eea21149c90..00000000000
--- a/changelogs/unreleased/fix-invalid-filename-eslint.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix invalid filename validation on eslint
-merge_request: 7281
-author:
diff --git a/changelogs/unreleased/fix-milestone-summary.yml b/changelogs/unreleased/fix-milestone-summary.yml
new file mode 100644
index 00000000000..3045a15054c
--- /dev/null
+++ b/changelogs/unreleased/fix-milestone-summary.yml
@@ -0,0 +1,4 @@
+---
+title: Displays milestone remaining days only when it's present
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-rename-mwbs-to-merge-when-pipeline-succeeds.yml b/changelogs/unreleased/fix-rename-mwbs-to-merge-when-pipeline-succeeds.yml
new file mode 100644
index 00000000000..f8acc6ef8ad
--- /dev/null
+++ b/changelogs/unreleased/fix-rename-mwbs-to-merge-when-pipeline-succeeds.yml
@@ -0,0 +1,4 @@
+---
+title: Rename Merge When Build Succeeds to Merge When Pipeline Succeeds
+merge_request: 7135
+author:
diff --git a/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml b/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml
new file mode 100644
index 00000000000..aa5ad5cd8d6
--- /dev/null
+++ b/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Slack pipeline message from pipelines made by API
+merge_request: 8059
+author:
diff --git a/changelogs/unreleased/gem-update-grape.yml b/changelogs/unreleased/gem-update-grape.yml
new file mode 100644
index 00000000000..46b6702d9fd
--- /dev/null
+++ b/changelogs/unreleased/gem-update-grape.yml
@@ -0,0 +1,4 @@
+---
+title: 'Gem update: Update grape to 0.18.0'
+merge_request:
+author: Robert Schilling
diff --git a/changelogs/unreleased/git-gc-improvements.yml b/changelogs/unreleased/git-gc-improvements.yml
deleted file mode 100644
index f15e667ce87..00000000000
--- a/changelogs/unreleased/git-gc-improvements.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Finer-grained Git gargage collection
-merge_request: 6588
-author:
diff --git a/changelogs/unreleased/gitlab-workhorse-multipart.yml b/changelogs/unreleased/gitlab-workhorse-multipart.yml
new file mode 100644
index 00000000000..23c2139cf93
--- /dev/null
+++ b/changelogs/unreleased/gitlab-workhorse-multipart.yml
@@ -0,0 +1,4 @@
+---
+title: Replace Rack::Multipart with GitLab-Workhorse based solution
+merge_request: 5867
+author:
diff --git a/changelogs/unreleased/glm-shorthand-reference.yml b/changelogs/unreleased/glm-shorthand-reference.yml
new file mode 100644
index 00000000000..6d60f23c798
--- /dev/null
+++ b/changelogs/unreleased/glm-shorthand-reference.yml
@@ -0,0 +1,4 @@
+---
+title: Add shorthand support to gitlab markdown references
+merge_request: 7255
+author: Oswaldo Ferreira
diff --git a/changelogs/unreleased/group-members-in-project-members-view.yml b/changelogs/unreleased/group-members-in-project-members-view.yml
new file mode 100644
index 00000000000..415e2b6b1e2
--- /dev/null
+++ b/changelogs/unreleased/group-members-in-project-members-view.yml
@@ -0,0 +1,4 @@
+---
+title: Shows group members in project members list
+merge_request:
+author:
diff --git a/changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml b/changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml
new file mode 100644
index 00000000000..73d8a52e001
--- /dev/null
+++ b/changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml
@@ -0,0 +1,4 @@
+---
+title: Add git diff context to notifications of new notes on merge requests
+merge_request:
+author: Heidi Hoopes
diff --git a/changelogs/unreleased/html-safe-diff-line-content.yml b/changelogs/unreleased/html-safe-diff-line-content.yml
new file mode 100644
index 00000000000..8f8bbc51963
--- /dev/null
+++ b/changelogs/unreleased/html-safe-diff-line-content.yml
@@ -0,0 +1,4 @@
+---
+title: Don't accidentally mark unsafe diff lines as HTML safe
+merge_request:
+author:
diff --git a/changelogs/unreleased/improve-invite-accept-page.yml b/changelogs/unreleased/improve-invite-accept-page.yml
new file mode 100644
index 00000000000..8a09a5ae42f
--- /dev/null
+++ b/changelogs/unreleased/improve-invite-accept-page.yml
@@ -0,0 +1,4 @@
+---
+title: Add note to the invite page when the logged in user email is not the same as the invitation
+merge_request:
+author:
diff --git a/changelogs/unreleased/issuable_filters_present-refactor.yml b/changelogs/unreleased/issuable_filters_present-refactor.yml
new file mode 100644
index 00000000000..c131f9cb68e
--- /dev/null
+++ b/changelogs/unreleased/issuable_filters_present-refactor.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor issuable_filters_present to reduce duplications
+merge_request: 7776
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/issue-24534.yml b/changelogs/unreleased/issue-24534.yml
new file mode 100644
index 00000000000..14d6730d3f6
--- /dev/null
+++ b/changelogs/unreleased/issue-24534.yml
@@ -0,0 +1,4 @@
+---
+title: Remove unnecessary sentences for status codes in the API documentation
+merge_request:
+author: Luis Alonso Chavez Armendariz
diff --git a/changelogs/unreleased/issue-boards-scrollable-element.yml b/changelogs/unreleased/issue-boards-scrollable-element.yml
new file mode 100644
index 00000000000..90edc30e791
--- /dev/null
+++ b/changelogs/unreleased/issue-boards-scrollable-element.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed issue boards scrolling with a lot of lists & issues
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue-events-filter.yml b/changelogs/unreleased/issue-events-filter.yml
new file mode 100644
index 00000000000..a3b08bde6e7
--- /dev/null
+++ b/changelogs/unreleased/issue-events-filter.yml
@@ -0,0 +1,4 @@
+---
+title: Add issue events filter and make all really show all events
+merge_request: 7673
+author: Oxan van Leeuwen
diff --git a/changelogs/unreleased/issue_13270.yml b/changelogs/unreleased/issue_13270.yml
new file mode 100644
index 00000000000..9c15c436876
--- /dev/null
+++ b/changelogs/unreleased/issue_13270.yml
@@ -0,0 +1,4 @@
+---
+title: Allow to delete tag release note
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_22269.yml b/changelogs/unreleased/issue_22269.yml
new file mode 100644
index 00000000000..6b7164aff77
--- /dev/null
+++ b/changelogs/unreleased/issue_22269.yml
@@ -0,0 +1,4 @@
+---
+title: Create mattermost service
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_23032.yml b/changelogs/unreleased/issue_23032.yml
deleted file mode 100644
index d376cf52112..00000000000
--- a/changelogs/unreleased/issue_23032.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow to test JIRA service settings without having a repository
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_24020.yml b/changelogs/unreleased/issue_24020.yml
new file mode 100644
index 00000000000..87310b75296
--- /dev/null
+++ b/changelogs/unreleased/issue_24020.yml
@@ -0,0 +1,4 @@
+---
+title: "fix display hook error message"
+merge_request: 7775
+author: basyura
diff --git a/changelogs/unreleased/issue_24363.yml b/changelogs/unreleased/issue_24363.yml
new file mode 100644
index 00000000000..0298890b477
--- /dev/null
+++ b/changelogs/unreleased/issue_24363.yml
@@ -0,0 +1,4 @@
+---
+title: Fix appearance in error pages
+merge_request:
+author: Luis Alonso Chavez Armendariz
diff --git a/changelogs/unreleased/issue_24748.yml b/changelogs/unreleased/issue_24748.yml
new file mode 100644
index 00000000000..4c1df542c53
--- /dev/null
+++ b/changelogs/unreleased/issue_24748.yml
@@ -0,0 +1,4 @@
+---
+title: Fix title case to sentence case
+merge_request:
+author: Luis Alonso Chavez Armendariz
diff --git a/changelogs/unreleased/issue_24958.yml b/changelogs/unreleased/issue_24958.yml
new file mode 100644
index 00000000000..dbbbbf9d28d
--- /dev/null
+++ b/changelogs/unreleased/issue_24958.yml
@@ -0,0 +1,4 @@
+---
+title: Fix bad selection on dropdown menu for tags filter
+merge_request:
+author: Luis Alonso Chavez Armendariz
diff --git a/changelogs/unreleased/issue_25030.yml b/changelogs/unreleased/issue_25030.yml
new file mode 100644
index 00000000000..e18b8d6a79b
--- /dev/null
+++ b/changelogs/unreleased/issue_25030.yml
@@ -0,0 +1,4 @@
+---
+title: Allow branch names with dots on API endpoint
+merge_request:
+author:
diff --git a/changelogs/unreleased/issues-1608-text.yml b/changelogs/unreleased/issues-1608-text.yml
new file mode 100644
index 00000000000..bef427a1e1e
--- /dev/null
+++ b/changelogs/unreleased/issues-1608-text.yml
@@ -0,0 +1,4 @@
+---
+title: change text around timestamps to make it clear which timestamp is displayed
+merge_request: 7860
+author: BM5k
diff --git a/changelogs/unreleased/jej-22869.yml b/changelogs/unreleased/jej-22869.yml
new file mode 100644
index 00000000000..9d2edcfee42
--- /dev/null
+++ b/changelogs/unreleased/jej-22869.yml
@@ -0,0 +1,4 @@
+---
+title: Fix information disclosure in `Projects::BlobController#update`
+merge_request:
+author:
diff --git a/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml b/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml
new file mode 100644
index 00000000000..5a4a44b9562
--- /dev/null
+++ b/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml
@@ -0,0 +1,4 @@
+---
+title: Replace MR access checks with use of MergeRequestsFinder
+merge_request:
+author:
diff --git a/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml b/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml
new file mode 100644
index 00000000000..db1389e2024
--- /dev/null
+++ b/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml
@@ -0,0 +1,4 @@
+---
+title: Issue#visible_to_user moved to IssuesFinder to prevent accidental use
+merge_request:
+author:
diff --git a/changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml b/changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml
new file mode 100644
index 00000000000..844fba9a107
--- /dev/null
+++ b/changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml
@@ -0,0 +1,4 @@
+---
+title: Fix missing access checks on issue lookup using IssuableFinder
+merge_request:
+author:
diff --git a/changelogs/unreleased/jej-note-search-uses-finder.yml b/changelogs/unreleased/jej-note-search-uses-finder.yml
new file mode 100644
index 00000000000..1768bdfd487
--- /dev/null
+++ b/changelogs/unreleased/jej-note-search-uses-finder.yml
@@ -0,0 +1,4 @@
+---
+title: Fix missing Note access checks by moving Note#search to updated NoteFinder
+merge_request:
+author:
diff --git a/changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml b/changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml
new file mode 100644
index 00000000000..c0b6f50052c
--- /dev/null
+++ b/changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml
@@ -0,0 +1,4 @@
+---
+title: Replace issue access checks with use of IssuableFinder
+merge_request:
+author:
diff --git a/changelogs/unreleased/members-dropdowns.yml b/changelogs/unreleased/members-dropdowns.yml
new file mode 100644
index 00000000000..b15403d6d62
--- /dev/null
+++ b/changelogs/unreleased/members-dropdowns.yml
@@ -0,0 +1,4 @@
+---
+title: Updated members dropdowns
+merge_request:
+author:
diff --git a/changelogs/unreleased/milestone_start_date.yml b/changelogs/unreleased/milestone_start_date.yml
new file mode 100644
index 00000000000..39ac1344329
--- /dev/null
+++ b/changelogs/unreleased/milestone_start_date.yml
@@ -0,0 +1,4 @@
+---
+title: Add a starting date to milestones
+merge_request:
+author:
diff --git a/changelogs/unreleased/move-abuse-report-spinach-test-to-rspec.yml b/changelogs/unreleased/move-abuse-report-spinach-test-to-rspec.yml
new file mode 100644
index 00000000000..9de7477c200
--- /dev/null
+++ b/changelogs/unreleased/move-abuse-report-spinach-test-to-rspec.yml
@@ -0,0 +1,4 @@
+---
+title: Move abuse report spinach test to rspec
+merge_request: 7659
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/move-admin-abuse-report-spinach-test-to-rspec.yml b/changelogs/unreleased/move-admin-abuse-report-spinach-test-to-rspec.yml
new file mode 100644
index 00000000000..fb70fa2955a
--- /dev/null
+++ b/changelogs/unreleased/move-admin-abuse-report-spinach-test-to-rspec.yml
@@ -0,0 +1,4 @@
+---
+title: Move admin abuse report spinach test to rspec
+merge_request: 7691
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml b/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml
new file mode 100644
index 00000000000..11250643a23
--- /dev/null
+++ b/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml
@@ -0,0 +1,4 @@
+---
+title: Move admin active tab spinach tests to rspec
+merge_request: 8037
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/move-admin-hooks-spinach-test-to-rspec.yml b/changelogs/unreleased/move-admin-hooks-spinach-test-to-rspec.yml
new file mode 100644
index 00000000000..7dfd741985a
--- /dev/null
+++ b/changelogs/unreleased/move-admin-hooks-spinach-test-to-rspec.yml
@@ -0,0 +1,4 @@
+---
+title: Move admin hooks spinach to rspec
+merge_request: 7942
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/move-admin-logs-spinach-test-to-rspec.yml b/changelogs/unreleased/move-admin-logs-spinach-test-to-rspec.yml
new file mode 100644
index 00000000000..696aa8510a0
--- /dev/null
+++ b/changelogs/unreleased/move-admin-logs-spinach-test-to-rspec.yml
@@ -0,0 +1,4 @@
+---
+title: Move admin logs spinach test to rspec
+merge_request: 7945
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/move-admin-spam-spinach-test-to-rspec.yml b/changelogs/unreleased/move-admin-spam-spinach-test-to-rspec.yml
new file mode 100644
index 00000000000..a7ec2c20554
--- /dev/null
+++ b/changelogs/unreleased/move-admin-spam-spinach-test-to-rspec.yml
@@ -0,0 +1,4 @@
+---
+title: Move admin spam spinach test to Rspec
+merge_request: 7708
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/mr-origin-7855.yml b/changelogs/unreleased/mr-origin-7855.yml
new file mode 100644
index 00000000000..0fdc6153d55
--- /dev/null
+++ b/changelogs/unreleased/mr-origin-7855.yml
@@ -0,0 +1,4 @@
+---
+title: Provides a sensible default message when adding a README to a project
+merge_request: 7903
+author:
diff --git a/changelogs/unreleased/post_receive-any-email.yml b/changelogs/unreleased/post_receive-any-email.yml
new file mode 100644
index 00000000000..3710b1b4b46
--- /dev/null
+++ b/changelogs/unreleased/post_receive-any-email.yml
@@ -0,0 +1,4 @@
+---
+title: "post_receive: accept any user email from last commit"
+merge_request: 7225
+author: Elan Ruusamäe
diff --git a/changelogs/unreleased/process-commit-worker-migration-encoding.yml b/changelogs/unreleased/process-commit-worker-migration-encoding.yml
new file mode 100644
index 00000000000..26aabd9b647
--- /dev/null
+++ b/changelogs/unreleased/process-commit-worker-migration-encoding.yml
@@ -0,0 +1,4 @@
+---
+title: Encode input when migrating ProcessCommitWorker jobs to prevent migration errors
+merge_request:
+author:
diff --git a/changelogs/unreleased/process-commits-using-sidekiq.yml b/changelogs/unreleased/process-commits-using-sidekiq.yml
deleted file mode 100644
index 9f596e6a584..00000000000
--- a/changelogs/unreleased/process-commits-using-sidekiq.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Process commits using a dedicated Sidekiq worker
-merge_request: 6802
-author:
diff --git a/changelogs/unreleased/public-tags-api.yml b/changelogs/unreleased/public-tags-api.yml
new file mode 100644
index 00000000000..f5e844470b2
--- /dev/null
+++ b/changelogs/unreleased/public-tags-api.yml
@@ -0,0 +1,4 @@
+---
+title: Allow public access to some Tag API endpoints
+merge_request:
+author:
diff --git a/changelogs/unreleased/readme-link-fix.yml b/changelogs/unreleased/readme-link-fix.yml
new file mode 100644
index 00000000000..211d3b80c3a
--- /dev/null
+++ b/changelogs/unreleased/readme-link-fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fix broken README.md UX guide link.
+merge_request:
+author:
diff --git a/changelogs/unreleased/refactor-create-service-spec.yml b/changelogs/unreleased/refactor-create-service-spec.yml
new file mode 100644
index 00000000000..148a0fee02c
--- /dev/null
+++ b/changelogs/unreleased/refactor-create-service-spec.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor create service spec
+merge_request: 7609
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/remove-backup-strategies.yml b/changelogs/unreleased/remove-backup-strategies.yml
new file mode 100644
index 00000000000..9f034613c2c
--- /dev/null
+++ b/changelogs/unreleased/remove-backup-strategies.yml
@@ -0,0 +1,4 @@
+---
+title: Stop supporting Google and Azure as backup strategies
+merge_request:
+author:
diff --git a/changelogs/unreleased/remove-jsx-react-eslint-plugins.yml b/changelogs/unreleased/remove-jsx-react-eslint-plugins.yml
new file mode 100644
index 00000000000..6e02998b3a8
--- /dev/null
+++ b/changelogs/unreleased/remove-jsx-react-eslint-plugins.yml
@@ -0,0 +1,5 @@
+---
+title: Changed eslint airbnb config to the base airbnb config and corrected eslintrc
+ plugins and envs
+merge_request: 7470
+author: Luke "Jared" Bennett
diff --git a/changelogs/unreleased/remove-require-from-services.yml b/changelogs/unreleased/remove-require-from-services.yml
new file mode 100644
index 00000000000..400512e0314
--- /dev/null
+++ b/changelogs/unreleased/remove-require-from-services.yml
@@ -0,0 +1,4 @@
+---
+title: 'Remove unnecessary require_relative calls from service classes'
+merge_request: '7601'
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml b/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml
new file mode 100644
index 00000000000..754af641add
--- /dev/null
+++ b/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml
@@ -0,0 +1,4 @@
+---
+title: Remove unnecessary commits order message
+merge_request: 8004
+author:
diff --git a/changelogs/unreleased/remove-unnecessary-self-from-user-model.yml b/changelogs/unreleased/remove-unnecessary-self-from-user-model.yml
new file mode 100644
index 00000000000..bef11c63675
--- /dev/null
+++ b/changelogs/unreleased/remove-unnecessary-self-from-user-model.yml
@@ -0,0 +1,4 @@
+---
+title: Remove unnecessary self from user model
+merge_request: 7551
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/removing_unnecessary_indexes.yml b/changelogs/unreleased/removing_unnecessary_indexes.yml
new file mode 100644
index 00000000000..01314ab5585
--- /dev/null
+++ b/changelogs/unreleased/removing_unnecessary_indexes.yml
@@ -0,0 +1,4 @@
+---
+title: Remove unnecessary database indices
+merge_request:
+author:
diff --git a/changelogs/unreleased/render-svg-in-diffs-and-notes.yml b/changelogs/unreleased/render-svg-in-diffs-and-notes.yml
new file mode 100644
index 00000000000..827b0dbb1d3
--- /dev/null
+++ b/changelogs/unreleased/render-svg-in-diffs-and-notes.yml
@@ -0,0 +1,4 @@
+---
+title: Render SVG images in diffs and notes
+merge_request: 7747
+author: andrebsguedes
diff --git a/changelogs/unreleased/repository-name-emojis b/changelogs/unreleased/repository-name-emojis
new file mode 100644
index 00000000000..fe52df8eedc
--- /dev/null
+++ b/changelogs/unreleased/repository-name-emojis
@@ -0,0 +1,4 @@
+---
+title: Added ability to put emojis into repository name
+merge_request: 7420
+author: Vincent Composieux
diff --git a/changelogs/unreleased/right-sidebar-fixture.yml b/changelogs/unreleased/right-sidebar-fixture.yml
new file mode 100644
index 00000000000..46a3e459fef
--- /dev/null
+++ b/changelogs/unreleased/right-sidebar-fixture.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for right_sidebar_spec
+merge_request: 7687
+author: winniehell
diff --git a/changelogs/unreleased/rounded-labels-fixes.yml b/changelogs/unreleased/rounded-labels-fixes.yml
new file mode 100644
index 00000000000..e0fbc6e3b5a
--- /dev/null
+++ b/changelogs/unreleased/rounded-labels-fixes.yml
@@ -0,0 +1,4 @@
+---
+title: Additional rounded label fixes
+merge_request:
+author:
diff --git a/changelogs/unreleased/rs-project-team-helpers.yml b/changelogs/unreleased/rs-project-team-helpers.yml
new file mode 100644
index 00000000000..79abcbce1e3
--- /dev/null
+++ b/changelogs/unreleased/rs-project-team-helpers.yml
@@ -0,0 +1,4 @@
+---
+title: Add shortcuts for adding users to a project team with a specific role
+merge_request:
+author: Nikolay Ponomarev and Dino M
diff --git a/changelogs/unreleased/seed-runner-token.yml b/changelogs/unreleased/seed-runner-token.yml
new file mode 100644
index 00000000000..e8153be043a
--- /dev/null
+++ b/changelogs/unreleased/seed-runner-token.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for setting the GitLab Runners Registration Token during initial
+ database seeding
+merge_request: 6642
+author:
diff --git a/changelogs/unreleased/sh-bump-omniauth-gitlab.yml b/changelogs/unreleased/sh-bump-omniauth-gitlab.yml
deleted file mode 100644
index 17cd5a993dd..00000000000
--- a/changelogs/unreleased/sh-bump-omniauth-gitlab.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Bump omniauth-gitlab to 1.0.2 to fix incompatibility with omniauth-oauth2
-merge_request:
-author:
diff --git a/changelogs/unreleased/shortcuts-issuable-fixture.yml b/changelogs/unreleased/shortcuts-issuable-fixture.yml
new file mode 100644
index 00000000000..88945600886
--- /dev/null
+++ b/changelogs/unreleased/shortcuts-issuable-fixture.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for shortcuts_issuable_spec
+merge_request: 7685
+author: winniehell
diff --git a/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml b/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml
new file mode 100644
index 00000000000..bbd7a217493
--- /dev/null
+++ b/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml
@@ -0,0 +1,4 @@
+---
+title: Show commit status from latest pipeline
+merge_request: 7333
+author:
diff --git a/changelogs/unreleased/show-status-from-branch.yml b/changelogs/unreleased/show-status-from-branch.yml
deleted file mode 100644
index 1afc230c05c..00000000000
--- a/changelogs/unreleased/show-status-from-branch.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix showing pipeline status for a given commit from correct branch
-merge_request: 7034
-author:
diff --git a/changelogs/unreleased/sidekiq_default_retries.yml b/changelogs/unreleased/sidekiq_default_retries.yml
deleted file mode 100644
index 3df2a415dbc..00000000000
--- a/changelogs/unreleased/sidekiq_default_retries.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Set default Sidekiq retries to 3
-merge_request: 7294
-author:
diff --git a/changelogs/unreleased/simplify-create-new-list-issue-boards.yml b/changelogs/unreleased/simplify-create-new-list-issue-boards.yml
new file mode 100644
index 00000000000..ca11e3b94a7
--- /dev/null
+++ b/changelogs/unreleased/simplify-create-new-list-issue-boards.yml
@@ -0,0 +1,4 @@
+---
+title: Simplify copy on "Create a new list" dropdown in Issue Boards
+merge_request: 7605
+author: Victor Rodrigues
diff --git a/changelogs/unreleased/small-emoji-adjustments.yml b/changelogs/unreleased/small-emoji-adjustments.yml
new file mode 100644
index 00000000000..804bd05b613
--- /dev/null
+++ b/changelogs/unreleased/small-emoji-adjustments.yml
@@ -0,0 +1,4 @@
+---
+title: Various small emoji positioning adjustments
+merge_request:
+author:
diff --git a/changelogs/unreleased/timeago-perf-fix.yml b/changelogs/unreleased/timeago-perf-fix.yml
new file mode 100644
index 00000000000..265e7db29a9
--- /dev/null
+++ b/changelogs/unreleased/timeago-perf-fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed timeago re-rendering every timeago
+merge_request:
+author:
diff --git a/changelogs/unreleased/unescape-relative-path.yml b/changelogs/unreleased/unescape-relative-path.yml
new file mode 100644
index 00000000000..755b0379a16
--- /dev/null
+++ b/changelogs/unreleased/unescape-relative-path.yml
@@ -0,0 +1,4 @@
+---
+title: Avoid escaping relative links in Markdown twice
+merge_request: 7940
+author: winniehell
diff --git a/changelogs/unreleased/update-api-spec-files.yml b/changelogs/unreleased/update-api-spec-files.yml
new file mode 100644
index 00000000000..349d866cf22
--- /dev/null
+++ b/changelogs/unreleased/update-api-spec-files.yml
@@ -0,0 +1,4 @@
+---
+title: Update API spec files to describe the correct class
+merge_request:
+author: Livier
diff --git a/changelogs/unreleased/update-button-font-weight.yml b/changelogs/unreleased/update-button-font-weight.yml
new file mode 100644
index 00000000000..ddb3c1c8da4
--- /dev/null
+++ b/changelogs/unreleased/update-button-font-weight.yml
@@ -0,0 +1,4 @@
+---
+title: Updates the font weight of button styles because of the change to system fonts
+merge_request:
+author:
diff --git a/changelogs/unreleased/update-git-version-in-doc.yml b/changelogs/unreleased/update-git-version-in-doc.yml
new file mode 100644
index 00000000000..cb3260f71cd
--- /dev/null
+++ b/changelogs/unreleased/update-git-version-in-doc.yml
@@ -0,0 +1,4 @@
+---
+title: Bump Git version requirement to 2.8.4
+merge_request:
+author:
diff --git a/changelogs/unreleased/use-separate-token-for-incoming-email.yml b/changelogs/unreleased/use-separate-token-for-incoming-email.yml
deleted file mode 100644
index e498f8dd0a6..00000000000
--- a/changelogs/unreleased/use-separate-token-for-incoming-email.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use separate email-token for incoming email and revert back the inactive feature
-merge_request: 5914
-author:
diff --git a/changelogs/unreleased/use-st-commits-where-possible.yml b/changelogs/unreleased/use-st-commits-where-possible.yml
new file mode 100644
index 00000000000..e4395461560
--- /dev/null
+++ b/changelogs/unreleased/use-st-commits-where-possible.yml
@@ -0,0 +1,5 @@
+---
+title: Replace references to MergeRequestDiff#commits with st_commits when we care
+ only about the number of commits
+merge_request: 7668
+author:
diff --git a/changelogs/unreleased/username-exists-root.yml b/changelogs/unreleased/username-exists-root.yml
new file mode 100644
index 00000000000..1ffb3eb435c
--- /dev/null
+++ b/changelogs/unreleased/username-exists-root.yml
@@ -0,0 +1,4 @@
+---
+title: Username exists check respects relative root path
+merge_request:
+author:
diff --git a/changelogs/unreleased/validate-state-param-when-filtering-issuables.yml b/changelogs/unreleased/validate-state-param-when-filtering-issuables.yml
new file mode 100644
index 00000000000..3fb025806b0
--- /dev/null
+++ b/changelogs/unreleased/validate-state-param-when-filtering-issuables.yml
@@ -0,0 +1,4 @@
+---
+title: Validate state param when filtering issuables
+merge_request:
+author:
diff --git a/changelogs/unreleased/zen-mode-fixture.yml b/changelogs/unreleased/zen-mode-fixture.yml
new file mode 100644
index 00000000000..bec6f6e6dba
--- /dev/null
+++ b/changelogs/unreleased/zen-mode-fixture.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for zen_mode_spec
+merge_request: 7686
+author: winniehell
diff --git a/changelogs/unreleased/zj-expose-coverage-pipelines.yml b/changelogs/unreleased/zj-expose-coverage-pipelines.yml
new file mode 100644
index 00000000000..34e4926e58a
--- /dev/null
+++ b/changelogs/unreleased/zj-expose-coverage-pipelines.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: expose pipeline coverage'
+merge_request:
+author:
diff --git a/changelogs/unreleased/zj-fix-label-creation-non-members.yml b/changelogs/unreleased/zj-fix-label-creation-non-members.yml
new file mode 100644
index 00000000000..ae4824f82fa
--- /dev/null
+++ b/changelogs/unreleased/zj-fix-label-creation-non-members.yml
@@ -0,0 +1,4 @@
+---
+title: Non members cannot create labels through the API
+merge_request:
+author:
diff --git a/changelogs/unreleased/zj-guest-reads-public-builds.yml b/changelogs/unreleased/zj-guest-reads-public-builds.yml
new file mode 100644
index 00000000000..1859addd606
--- /dev/null
+++ b/changelogs/unreleased/zj-guest-reads-public-builds.yml
@@ -0,0 +1,4 @@
+---
+title: Guests can read builds when public
+merge_request: 6842
+author:
diff --git a/changelogs/unreleased/zj-issue-new-over-issue-create.yml b/changelogs/unreleased/zj-issue-new-over-issue-create.yml
new file mode 100644
index 00000000000..9dd463e4efa
--- /dev/null
+++ b/changelogs/unreleased/zj-issue-new-over-issue-create.yml
@@ -0,0 +1,4 @@
+---
+title: Accept issue new as command to create an issue
+merge_request:
+author:
diff --git a/changelogs/unreleased/zj-issue-search-slash-command.yml b/changelogs/unreleased/zj-issue-search-slash-command.yml
new file mode 100644
index 00000000000..de41c39d545
--- /dev/null
+++ b/changelogs/unreleased/zj-issue-search-slash-command.yml
@@ -0,0 +1,4 @@
+---
+title: Add issue search slash command
+merge_request:
+author:
diff --git a/changelogs/unreleased/zj-use-ruby-2-3-3.yml b/changelogs/unreleased/zj-use-ruby-2-3-3.yml
new file mode 100644
index 00000000000..0d1a0fcd79d
--- /dev/null
+++ b/changelogs/unreleased/zj-use-ruby-2-3-3.yml
@@ -0,0 +1,4 @@
+---
+title: Bump ruby version to 2.3.3
+merge_request: 7904
+author:
diff --git a/config/README.md b/config/README.md
new file mode 100644
index 00000000000..0a5ea2424e0
--- /dev/null
+++ b/config/README.md
@@ -0,0 +1,22 @@
+# Configuration files Documentation
+
+Note that most configuration files (`config/*.*`) committed into
+[gitlab-ce](https://gitlab.com/gitlab-org/gitlab-ce) **will not be used** for
+[omnibus-gitlab](https://gitlab.com/gitlab-org/omnibus-gitlab). Configuration
+files committed into gitlab-ce are only used for development.
+
+## gitlab.yml
+
+You can find most of GitLab configuration settings here.
+
+## mail_room.yml
+
+This file is actually an YML wrapped inside an ERB file to enable templated
+values to be specified from `gitlab.yml`. mail_room loads this file first as
+an ERB file and then loads the resulting YML as its configuration.
+
+## resque.yml
+
+This file is called `resque.yml` for historical reasons. We are **NOT**
+using Resque at the moment. It is used to specify Redis configuration
+values instead.
diff --git a/config/application.rb b/config/application.rb
index 946b632b0e8..782a7a36895 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -45,7 +45,7 @@ module Gitlab
#
# Parameters filtered:
# - Password (:password, :password_confirmation)
- # - Private tokens (:private_token)
+ # - Private tokens
# - Two-factor tokens (:otp_attempt)
# - Repo/Project Import URLs (:import_url)
# - Build variables (:variables)
@@ -55,15 +55,18 @@ module Gitlab
# - Sentry DSN (:sentry_dsn)
# - Deploy keys (:key)
config.filter_parameters += %i(
+ authentication_token
certificate
encrypted_key
hook
import_url
+ incoming_email_token
key
otp_attempt
password
password_confirmation
private_token
+ runners_token
secret_token
sentry_dsn
variables
@@ -84,6 +87,8 @@ module Gitlab
config.assets.precompile << "print.css"
config.assets.precompile << "notify.css"
config.assets.precompile << "mailers/*.css"
+ config.assets.precompile << "katex.css"
+ config.assets.precompile << "katex.js"
config.assets.precompile << "graphs/graphs_bundle.js"
config.assets.precompile << "users/users_bundle.js"
config.assets.precompile << "network/network_bundle.js"
@@ -94,6 +99,7 @@ module Gitlab
config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js"
config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js"
config.assets.precompile << "boards/test_utils/simulate_drag.js"
+ config.assets.precompile << "environments/environments_bundle.js"
config.assets.precompile << "blob_edit/blob_edit_bundle.js"
config.assets.precompile << "snippet/snippet_bundle.js"
config.assets.precompile << "lib/utils/*.js"
diff --git a/config/database.yml.mysql b/config/database.yml.mysql
index a99c50706c5..d9702870249 100644
--- a/config/database.yml.mysql
+++ b/config/database.yml.mysql
@@ -3,8 +3,8 @@
#
production:
adapter: mysql2
- encoding: utf8
- collation: utf8_general_ci
+ encoding: utf8mb4
+ collation: utf8mb4_general_ci
reconnect: false
database: gitlabhq_production
pool: 10
@@ -18,8 +18,8 @@ production:
#
development:
adapter: mysql2
- encoding: utf8
- collation: utf8_general_ci
+ encoding: utf8mb4
+ collation: utf8mb4_general_ci
reconnect: false
database: gitlabhq_development
pool: 5
@@ -32,8 +32,8 @@ development:
# Do not set this db to the same as development or production.
test: &test
adapter: mysql2
- encoding: utf8
- collation: utf8_general_ci
+ encoding: utf8mb4
+ collation: utf8mb4_general_ci
reconnect: false
database: gitlabhq_test
pool: 5
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 699ab6075b6..b8b41a0d86c 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -138,6 +138,8 @@ production: &base
# The mailbox where incoming mail will end up. Usually "inbox".
mailbox: "inbox"
+ # The IDLE command timeout.
+ idle_timeout: 60
## Build Artifacts
artifacts:
@@ -151,6 +153,12 @@ production: &base
# The location where LFS objects are stored (default: shared/lfs-objects).
# storage_path: shared/lfs-objects
+ ## Mattermost
+ ## For enabling Add to Mattermost button
+ mattermost:
+ enabled: false
+ host: 'https://mattermost.example.com'
+
## Gravatar
## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html
gravatar:
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 9fec2ad6bf7..ddea325c6ca 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -215,6 +215,7 @@ Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(
Settings.gitlab['domain_whitelist'] ||= []
Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fogbugz git gitlab_project]
Settings.gitlab['trusted_proxies'] ||= []
+Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml'))
#
# CI
@@ -261,6 +262,13 @@ Settings.lfs['enabled'] = true if Settings.lfs['enabled'].nil?
Settings.lfs['storage_path'] = File.expand_path(Settings.lfs['storage_path'] || File.join(Settings.shared['path'], "lfs-objects"), Rails.root)
#
+# Mattermost
+#
+Settings['mattermost'] ||= Settingslogic.new({})
+Settings.mattermost['enabled'] = false if Settings.mattermost['enabled'].nil?
+Settings.mattermost['host'] = nil unless Settings.mattermost.enabled
+
+#
# Gravatar
#
Settings['gravatar'] ||= Settingslogic.new({})
@@ -301,7 +309,7 @@ Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({}
Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *'
Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker'
Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({})
-Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '* */6 * * *'
+Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '0 */6 * * *'
Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker'
Settings.cron_jobs['trending_projects_worker'] ||= Settingslogic.new({})
diff --git a/config/initializers/ar_monkey_patch.rb b/config/initializers/ar_monkey_patch.rb
index 0da584626ee..6979f4641b0 100644
--- a/config/initializers/ar_monkey_patch.rb
+++ b/config/initializers/ar_monkey_patch.rb
@@ -52,6 +52,23 @@ module ActiveRecord
raise
end
end
+
+ # This is patched because we need it to query `lock_version IS NULL`
+ # rather than `lock_version = 0` whenever lock_version is NULL.
+ def relation_for_destroy
+ return super unless locking_enabled?
+
+ column_name = self.class.locking_column
+ super.where(self.class.arel_table[column_name].eq(self[column_name]))
+ end
+ end
+
+ # This is patched because we want `lock_version` default to `NULL`
+ # rather than `0`
+ class LockingType < SimpleDelegator
+ def type_cast_from_database(value)
+ super
+ end
end
end
end
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index a0a8f88584c..a8afc36fc78 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -213,22 +213,9 @@ Devise.setup do |config|
end
if Gitlab::LDAP::Config.enabled?
- Gitlab.config.ldap.servers.values.each do |server|
- if server['allow_username_or_email_login']
- email_stripping_proc = ->(name) {name.gsub(/@.*\z/, '')}
- else
- email_stripping_proc = ->(name) {name}
- end
-
- config.omniauth server['provider_name'],
- host: server['host'],
- base: server['base'],
- uid: server['uid'],
- port: server['port'],
- method: server['method'],
- bind_dn: server['bind_dn'],
- password: server['password'],
- name_proc: email_stripping_proc
+ Gitlab::LDAP::Config.providers.each do |provider|
+ ldap_config = Gitlab::LDAP::Config.new(provider)
+ config.omniauth(provider, ldap_config.omniauth_options)
end
end
@@ -254,6 +241,10 @@ Devise.setup do |config|
end
end
+ if provider['name'] == 'shibboleth'
+ provider['args'][:fail_with_empty_uid] = true
+ end
+
# A Hash from the configuration will be passed as is.
provider_arguments << provider['args'].symbolize_keys
end
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index fc4b0a72add..88cd0f5f652 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -52,8 +52,8 @@ Doorkeeper.configure do
# Define access token scopes for your provider
# For more information go to
# https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes
- default_scopes :api
- # optional_scopes :write, :update
+ default_scopes(*Gitlab::Auth::DEFAULT_SCOPES)
+ optional_scopes(*Gitlab::Auth::OPTIONAL_SCOPES)
# Change the way client credentials are retrieved from the request object.
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
diff --git a/config/initializers/email_template_interceptor.rb b/config/initializers/email_template_interceptor.rb
new file mode 100644
index 00000000000..f195ca9bcd6
--- /dev/null
+++ b/config/initializers/email_template_interceptor.rb
@@ -0,0 +1,2 @@
+# Interceptor in lib/email_template_interceptor.rb
+ActionMailer::Base.register_interceptor(EmailTemplateInterceptor)
diff --git a/config/initializers/math_lexer.rb b/config/initializers/math_lexer.rb
new file mode 100644
index 00000000000..8a3388a267e
--- /dev/null
+++ b/config/initializers/math_lexer.rb
@@ -0,0 +1,2 @@
+# Touch the lexers so it is registered with Rouge
+Rouge::Lexers::Math
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index 26c30e523a7..ab5a0561b8c 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -26,3 +26,9 @@ if Gitlab.config.omniauth.enabled
end
end
end
+
+module OmniAuth
+ module Strategies
+ autoload :Bitbucket, Rails.root.join('lib', 'omniauth', 'strategies', 'bitbucket')
+ end
+end
diff --git a/config/initializers/public_key.rb b/config/initializers/public_key.rb
deleted file mode 100644
index e4f09a2d020..00000000000
--- a/config/initializers/public_key.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-path = File.expand_path("~/.ssh/bitbucket_rsa.pub")
-Gitlab::BitbucketImport.public_key = File.read(path) if File.exist?(path)
diff --git a/config/initializers/rack_attack_logging.rb b/config/initializers/rack_attack_logging.rb
new file mode 100644
index 00000000000..8bb9ea29c33
--- /dev/null
+++ b/config/initializers/rack_attack_logging.rb
@@ -0,0 +1,7 @@
+# Adds logging for all Rack Attack blocks and throttling events.
+
+ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, request_id, req|
+ if [:throttle, :blacklist].include? req.env['rack.attack.match_type']
+ Rails.logger.info("Rack_Attack: #{req.env['rack.attack.match_type']} #{req.ip} #{req.request_method} #{req.fullpath}")
+ end
+end
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
index 4f30d1265c8..6b0cff75653 100644
--- a/config/initializers/sentry.rb
+++ b/config/initializers/sentry.rb
@@ -15,7 +15,7 @@ if Rails.env.production?
Raven.configure do |config|
config.dsn = current_application_settings.sentry_dsn
config.release = Gitlab::REVISION
-
+
# Sanitize fields based on those sanitized from Rails.
config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s)
# Sanitize authentication headers
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 023af2af23c..5a7365bb0f6 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -29,10 +29,12 @@ Sidekiq.configure_server do |config|
end
Sidekiq::Cron::Job.load_from_hash! cron_jobs
+ Gitlab::SidekiqThrottler.execute!
+
# Database pool should be at least `sidekiq_concurrency` + 2
# For more info, see: https://github.com/mperham/sidekiq/blob/master/4.0-Upgrade.md
config = ActiveRecord::Base.configurations[Rails.env] ||
- Rails.application.config.database_configuration[Rails.env]
+ Rails.application.config.database_configuration[Rails.env]
config['pool'] = Sidekiq.options[:concurrency] + 2
ActiveRecord::Base.establish_connection(config)
Rails.logger.debug("Connection Pool size for Sidekiq Server is now: #{ActiveRecord::Base.connection.pool.instance_variable_get('@size')}")
@@ -59,5 +61,5 @@ begin
end
end
end
-rescue Redis::BaseError, SocketError
+rescue Redis::BaseError, SocketError, Errno::ENOENT, Errno::EAFNOSUPPORT, Errno::ECONNRESET, Errno::ECONNREFUSED
end
diff --git a/config/initializers/workhorse_multipart.rb b/config/initializers/workhorse_multipart.rb
new file mode 100644
index 00000000000..3e2f25c354a
--- /dev/null
+++ b/config/initializers/workhorse_multipart.rb
@@ -0,0 +1,3 @@
+Rails.application.configure do |config|
+ config.middleware.use(Gitlab::Middleware::Multipart)
+end
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
index a4032a21420..1d728282d90 100644
--- a/config/locales/doorkeeper.en.yml
+++ b/config/locales/doorkeeper.en.yml
@@ -59,6 +59,7 @@ en:
unknown: "The access token is invalid"
scopes:
api: Access your API
+ read_user: Read user information
flash:
applications:
diff --git a/config/mail_room.yml b/config/mail_room.yml
index b026d510f1b..774c5350a45 100644
--- a/config/mail_room.yml
+++ b/config/mail_room.yml
@@ -15,7 +15,7 @@
:start_tls: <%= config[:start_tls].to_json %>
:email: <%= config[:user].to_json %>
:password: <%= config[:password].to_json %>
- :idle_timeout: 60
+ :idle_timeout: <%= config[:idle_timeout].to_json %>
:name: <%= config[:mailbox].to_json %>
diff --git a/config/no_todos_messages.yml b/config/no_todos_messages.yml
new file mode 100644
index 00000000000..264a975b614
--- /dev/null
+++ b/config/no_todos_messages.yml
@@ -0,0 +1,11 @@
+# When the todo list on the user's dashboard becomes empty, a random message
+# from the list below will be shown.
+#
+# If you come up with a fun one, please feel free to contribute it to GitLab!
+# https://about.gitlab.com/contributing/
+---
+- Good job! Looks like you don't have any todos left.
+- Isn't an empty todo list beautiful?
+- Give yourself a pat on the back!
+- Nothing left to do, high five!
+- Henceforth you shall be known as "Todo Destroyer".
diff --git a/config/routes.rb b/config/routes.rb
index 7bf6c03e69b..06d565df469 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,6 +1,6 @@
require 'sidekiq/web'
require 'sidekiq/cron/web'
-require 'api/api'
+require 'constraints/group_url_constrainer'
Rails.application.routes.draw do
concern :access_requestable do
@@ -78,10 +78,21 @@ Rails.application.routes.draw do
draw :user
draw :project
- # Get all keys of user
- get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: /.*/ }
-
root to: "root#index"
- get '*unmatched_route', to: 'application#not_found'
+ # Since group show page is wildcard routing
+ # we want all other routing to be checked before matching this one
+ constraints(GroupUrlConstrainer.new) do
+ scope(path: '*id',
+ as: :group,
+ constraints: { id: Gitlab::Regex.namespace_route_regex, format: /(html|json|atom)/ },
+ controller: :groups) do
+ get '/', action: :show
+ patch '/', action: :update
+ put '/', action: :update
+ delete '/', action: :destroy
+ end
+ end
+
+ get '*unmatched_route', to: 'application#route_not_found'
end
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 5ae985da561..0dd2c8f7aef 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -28,9 +28,19 @@ namespace :admin do
resources :applications
- resources :groups, constraints: { id: /[^\/]+/ } do
- member do
+ resources :groups, only: [:index, :new, :create]
+
+ scope(path: 'groups/*id',
+ controller: :groups,
+ constraints: { id: Gitlab::Regex.namespace_route_regex }) do
+
+ scope(as: :group) do
put :members_update
+ get :edit, action: :edit
+ get '/', action: :show
+ patch '/', action: :update
+ put '/', action: :update
+ delete '/', action: :destroy
end
end
@@ -50,14 +60,13 @@ namespace :admin do
resource :system_info, controller: 'system_info', only: [:show]
resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ }
- resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
- root to: 'projects#index', as: :projects
+ resources :projects, only: [:index]
+ scope(path: 'projects/*namespace_id', as: :namespace) do
resources(:projects,
path: '/',
- constraints: { id: /[a-zA-Z.0-9_\-]+/ },
- only: [:index, :show]) do
- root to: 'projects#show'
+ constraints: { id: Gitlab::Regex.project_route_regex },
+ only: [:show]) do
member do
put :transfer
diff --git a/config/routes/git_http.rb b/config/routes/git_http.rb
index 03adc4815f3..42d874eeebc 100644
--- a/config/routes/git_http.rb
+++ b/config/routes/git_http.rb
@@ -1,37 +1,47 @@
-scope constraints: { id: /.+\.git/, format: nil } do
- # Git HTTP clients ('git clone' etc.)
- get '/info/refs', to: 'git_http#info_refs'
- post '/git-upload-pack', to: 'git_http#git_upload_pack'
- post '/git-receive-pack', to: 'git_http#git_receive_pack'
+scope(path: '*namespace_id/:project_id', constraints: { format: nil }) do
+ scope(constraints: { project_id: Gitlab::Regex.project_git_route_regex }, module: :projects) do
+ # Git HTTP clients ('git clone' etc.)
+ scope(controller: :git_http) do
+ get '/info/refs', action: :info_refs
+ post '/git-upload-pack', action: :git_upload_pack
+ post '/git-receive-pack', action: :git_receive_pack
+ end
- # Git LFS API (metadata)
- post '/info/lfs/objects/batch', to: 'lfs_api#batch'
- post '/info/lfs/objects', to: 'lfs_api#deprecated'
- get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated'
+ # Git LFS API (metadata)
+ scope(path: 'info/lfs/objects', controller: :lfs_api) do
+ post :batch
+ post '/', action: :deprecated
+ get '/*oid', action: :deprecated
+ end
- # GitLab LFS object storage
- scope constraints: { oid: /[a-f0-9]{64}/ } do
- get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download'
+ # GitLab LFS object storage
+ scope(path: 'gitlab-lfs/objects/*oid', controller: :lfs_storage, constraints: { oid: /[a-f0-9]{64}/ }) do
+ get '/', action: :download
- scope constraints: { size: /[0-9]+/ } do
- put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize'
- put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize'
+ scope constraints: { size: /[0-9]+/ } do
+ put '/*size/authorize', action: :upload_authorize
+ put '/*size', action: :upload_finalize
+ end
end
end
-end
-# Allow /info/refs, /info/refs?service=git-upload-pack, and
-# /info/refs?service=git-receive-pack, but nothing else.
-#
-git_http_handshake = lambda do |request|
- request.query_string.blank? ||
- request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/)
-end
+ # Redirect /group/project/info/refs to /group/project.git/info/refs
+ scope(constraints: { project_id: Gitlab::Regex.project_route_regex }) do
+ # Allow /info/refs, /info/refs?service=git-upload-pack, and
+ # /info/refs?service=git-receive-pack, but nothing else.
+ #
+ git_http_handshake = lambda do |request|
+ ProjectUrlConstrainer.new.matches?(request) &&
+ (request.query_string.blank? ||
+ request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/))
+ end
-ref_redirect = redirect do |params, request|
- path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs"
- path << "?#{request.query_string}" unless request.query_string.blank?
- path
-end
+ ref_redirect = redirect do |params, request|
+ path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs"
+ path << "?#{request.query_string}" unless request.query_string.blank?
+ path
+ end
-get '/info/refs', constraints: git_http_handshake, to: ref_redirect
+ get '/info/refs', constraints: git_http_handshake, to: ref_redirect
+ end
+end
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 3c392f77ef6..776c31c9dac 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -1,28 +1,9 @@
-require 'constraints/group_url_constrainer'
-
-constraints(GroupUrlConstrainer.new) do
- scope(path: ':id',
- as: :group,
- constraints: { id: Gitlab::Regex.namespace_route_regex },
- controller: :groups) do
- get '/', action: :show
- patch '/', action: :update
- put '/', action: :update
- delete '/', action: :destroy
- end
-end
-
resources :groups, only: [:index, :new, :create]
-scope(path: 'groups/:id', controller: :groups) do
- get :edit, as: :edit_group
- get :issues, as: :issues_group
- get :merge_requests, as: :merge_requests_group
- get :projects, as: :projects_group
- get :activity, as: :activity_group
-end
-
-scope(path: 'groups/:group_id', module: :groups, as: :group) do
+scope(path: 'groups/*group_id',
+ module: :groups,
+ as: :group,
+ constraints: { group_id: Gitlab::Regex.namespace_route_regex }) do
resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
post :resend_invite, on: :member
delete :leave, on: :collection
@@ -30,8 +11,21 @@ scope(path: 'groups/:group_id', module: :groups, as: :group) do
resource :avatar, only: [:destroy]
resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
- resources :labels, except: [:show], constraints: { id: /\d+/ }
+
+ resources :labels, except: [:show] do
+ post :toggle_subscription, on: :member
+ end
+end
+
+scope(path: 'groups/*id',
+ controller: :groups,
+ constraints: { id: Gitlab::Regex.namespace_route_regex }) do
+ get :edit, as: :edit_group
+ get :issues, as: :issues_group
+ get :merge_requests, as: :merge_requests_group
+ get :projects, as: :projects_group
+ get :activity, as: :activity_group
end
# Must be last route in this file
-get 'groups/:id' => 'groups#show', as: :group_canonical
+get 'groups/*id' => 'groups#show', as: :group_canonical, constraints: { id: Gitlab::Regex.namespace_route_regex }
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index 52b9a565db8..6b91485da9e 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -23,6 +23,12 @@ resource :profile, only: [:show, :update] do
resource :preferences, only: [:show, :update]
resources :keys, only: [:index, :show, :new, :create, :destroy]
resources :emails, only: [:index, :create, :destroy]
+ resources :chat_names, only: [:index, :new, :create, :destroy] do
+ collection do
+ delete :deny
+ end
+ end
+
resource :avatar, only: [:destroy]
resources :personal_access_tokens, only: [:index, :create] do
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 82defb0ba71..e17d6bae10c 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -1,28 +1,27 @@
-resources :projects, constraints: { id: /[^\/]+/ }, only: [:index, :new, :create]
-
-resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
- resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, except:
- [:new, :create, :index], path: "/") do
- member do
- put :transfer
- delete :remove_fork
- post :archive
- post :unarchive
- post :housekeeping
- post :toggle_star
- post :preview_markdown
- post :export
- post :remove_export
- post :generate_new_export
- get :download_export
- get :autocomplete_sources
- get :activity
- get :refs
- put :new_issue_address
- end
+require 'constraints/project_url_constrainer'
+
+resources :projects, only: [:index, :new, :create]
+
+draw :git_http
+
+constraints(ProjectUrlConstrainer.new) do
+ scope(path: '*namespace_id', as: :namespace) do
+ scope(path: ':project_id',
+ constraints: { project_id: Gitlab::Regex.project_route_regex },
+ module: :projects,
+ as: :project) do
- scope module: :projects do
- draw :git_http
+ resources :autocomplete_sources, only: [] do
+ collection do
+ get 'emojis'
+ get 'members'
+ get 'issues'
+ get 'merge_requests'
+ get 'labels'
+ get 'milestones'
+ get 'commands'
+ end
+ end
#
# Templates
@@ -125,6 +124,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
end
resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
+ delete :merged_branches, controller: 'branches', action: :destroy_all_merged
resources :tags, only: [:index, :show, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do
resource :release, only: [:edit, :update]
end
@@ -141,6 +141,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
member do
post :cancel
post :retry
+ get :builds
end
end
@@ -152,6 +153,18 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
resource :cycle_analytics, only: [:show]
+ namespace :cycle_analytics do
+ scope :events, controller: 'events' do
+ get :issue
+ get :plan
+ get :code
+ get :test
+ get :review
+ get :staging
+ get :production
+ end
+ end
+
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
collection do
post :cancel_all
@@ -298,5 +311,27 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
draw :wiki
draw :repository
end
+
+ resources(:projects,
+ path: '/',
+ constraints: { id: Gitlab::Regex.project_route_regex },
+ only: [:edit, :show, :update, :destroy]) do
+ member do
+ put :transfer
+ delete :remove_fork
+ post :archive
+ post :unarchive
+ post :housekeeping
+ post :toggle_star
+ post :preview_markdown
+ post :export
+ post :remove_export
+ post :generate_new_export
+ get :download_export
+ get :activity
+ get :refs
+ put :new_issue_address
+ end
+ end
end
end
diff --git a/config/routes/repository.rb b/config/routes/repository.rb
index 76dcf113aea..f8966c5ae75 100644
--- a/config/routes/repository.rb
+++ b/config/routes/repository.rb
@@ -29,82 +29,60 @@ get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob'
put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob'
post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob'
-scope do
- get(
- '/blob/*id/diff',
- to: 'blob#diff',
- constraints: { id: /.+/, format: false },
- as: :blob_diff
- )
- get(
- '/blob/*id',
- to: 'blob#show',
- constraints: { id: /.+/, format: false },
- as: :blob
- )
- delete(
- '/blob/*id',
- to: 'blob#destroy',
- constraints: { id: /.+/, format: false }
- )
- put(
- '/blob/*id',
- to: 'blob#update',
- constraints: { id: /.+/, format: false }
- )
- post(
- '/blob/*id',
- to: 'blob#create',
- constraints: { id: /.+/, format: false }
- )
+scope('/blob/*id', as: :blob, controller: :blob, constraints: { id: /.+/, format: false }) do
+ get :diff
+ get '/', action: :show
+ delete '/', action: :destroy
+ post '/', action: :create
+ put '/', action: :update
+end
- get(
- '/raw/*id',
- to: 'raw#show',
- constraints: { id: /.+/, format: /(html|js)/ },
- as: :raw
- )
+get(
+ '/raw/*id',
+ to: 'raw#show',
+ constraints: { id: /.+/, format: /(html|js)/ },
+ as: :raw
+)
- get(
- '/tree/*id',
- to: 'tree#show',
- constraints: { id: /.+/, format: /(html|js)/ },
- as: :tree
- )
+get(
+ '/tree/*id',
+ to: 'tree#show',
+ constraints: { id: /.+/, format: /(html|js)/ },
+ as: :tree
+)
- get(
- '/find_file/*id',
- to: 'find_file#show',
- constraints: { id: /.+/, format: /html/ },
- as: :find_file
- )
+get(
+ '/find_file/*id',
+ to: 'find_file#show',
+ constraints: { id: /.+/, format: /html/ },
+ as: :find_file
+)
- get(
- '/files/*id',
- to: 'find_file#list',
- constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ },
- as: :files
- )
+get(
+ '/files/*id',
+ to: 'find_file#list',
+ constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ },
+ as: :files
+)
- post(
- '/create_dir/*id',
- to: 'tree#create_dir',
- constraints: { id: /.+/ },
- as: 'create_dir'
- )
+post(
+ '/create_dir/*id',
+ to: 'tree#create_dir',
+ constraints: { id: /.+/ },
+ as: 'create_dir'
+)
- get(
- '/blame/*id',
- to: 'blame#show',
- constraints: { id: /.+/, format: /(html|js)/ },
- as: :blame
- )
+get(
+ '/blame/*id',
+ to: 'blame#show',
+ constraints: { id: /.+/, format: /(html|js)/ },
+ as: :blame
+)
- # File/dir history
- get(
- '/commits/*id',
- to: 'commits#show',
- constraints: { id: /.+/, format: false },
- as: :commits
- )
-end
+# File/dir history
+get(
+ '/commits/*id',
+ to: 'commits#show',
+ constraints: { id: /.+/, format: false },
+ as: :commits
+)
diff --git a/config/routes/user.rb b/config/routes/user.rb
index dc1068af6f6..b064a15e802 100644
--- a/config/routes/user.rb
+++ b/config/routes/user.rb
@@ -12,6 +12,9 @@ devise_scope :user do
end
constraints(UserUrlConstrainer.new) do
+ # Get all keys of user
+ get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: Gitlab::Regex.namespace_route_regex }
+
scope(path: ':username',
as: :user,
constraints: { username: Gitlab::Regex.namespace_route_regex },
diff --git a/config/routes/wiki.rb b/config/routes/wiki.rb
index ecd4d395d66..dad746d59a1 100644
--- a/config/routes/wiki.rb
+++ b/config/routes/wiki.rb
@@ -1,16 +1,19 @@
WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID
-scope do
- # Order matters to give priority to these matches
- get '/wikis/git_access', to: 'wikis#git_access'
- get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages'
- post '/wikis', to: 'wikis#create'
+scope(controller: :wikis) do
+ scope(path: 'wikis', as: :wikis) do
+ get :git_access
+ get :pages
+ get '/', to: redirect('/%{namespace_id}/%{project_id}/wikis/home')
+ post '/', to: 'wikis#create'
+ end
- get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID
- get '/wikis/*id/edit', to: 'wikis#edit', as: 'wiki_edit', constraints: WIKI_SLUG_ID
-
- get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID
- delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID
- put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID
- post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown'
+ scope(path: 'wikis/*id', as: :wiki, constraints: WIKI_SLUG_ID, format: false) do
+ get :edit
+ get :history
+ post :preview_markdown
+ get '/', action: :show
+ put '/', action: :update
+ delete '/', action: :destroy
+ end
end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 0aec8aedf72..69136b73946 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -34,6 +34,8 @@
- [project_service, 1]
- [clear_database_cache, 1]
- [delete_user, 1]
+ - [delete_merged_branches, 1]
+ - [authorized_projects, 1]
- [expire_build_instance_artifacts, 1]
- [group_destroy, 1]
- [irker, 1]
diff --git a/config/unicorn.rb.example b/config/unicorn.rb.example
index e5058cebce8..40a16a32359 100644
--- a/config/unicorn.rb.example
+++ b/config/unicorn.rb.example
@@ -44,7 +44,7 @@ listen "127.0.0.1:8080", :tcp_nopush => true
# nuke workers after 30 seconds instead of 60 seconds (the default)
#
# NOTICE: git push over http depends on this value.
-# If you want be able to push huge amount of data to git repository over http
+# If you want to be able to push huge amount of data to git repository over http
# you will have to increase this value too.
#
# Example of output if you try to push 1GB repo to GitLab over http.
@@ -82,7 +82,7 @@ GC.respond_to?(:copy_on_write_friendly=) and
check_client_connection false
before_fork do |server, worker|
- # the following is highly recomended for Rails + "preload_app true"
+ # the following is highly recommended for Rails + "preload_app true"
# as there's no need for the master process to hold a connection
defined?(ActiveRecord::Base) and
ActiveRecord::Base.connection.disconnect!
diff --git a/db/fixtures/development/06_teams.rb b/db/fixtures/development/06_teams.rb
index 9739a5ac8d5..5c2a03fec3f 100644
--- a/db/fixtures/development/06_teams.rb
+++ b/db/fixtures/development/06_teams.rb
@@ -1,20 +1,24 @@
-Gitlab::Seeder.quiet do
- Group.all.each do |group|
- User.all.sample(4).each do |user|
- if group.add_user(user, Gitlab::Access.values.sample).persisted?
- print '.'
- else
- print 'F'
+require 'sidekiq/testing'
+
+Sidekiq::Testing.inline! do
+ Gitlab::Seeder.quiet do
+ Group.all.each do |group|
+ User.all.sample(4).each do |user|
+ if group.add_user(user, Gitlab::Access.values.sample).persisted?
+ print '.'
+ else
+ print 'F'
+ end
end
end
- end
- Project.all.each do |project|
- User.all.sample(4).each do |user|
- if project.team << [user, Gitlab::Access.values.sample]
- print '.'
- else
- print 'F'
+ Project.all.each do |project|
+ User.all.sample(4).each do |user|
+ if project.team << [user, Gitlab::Access.values.sample]
+ print '.'
+ else
+ print 'F'
+ end
end
end
end
diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb
index 08ad3097d34..19e001854d2 100644
--- a/db/fixtures/development/14_pipelines.rb
+++ b/db/fixtures/development/14_pipelines.rb
@@ -115,7 +115,7 @@ class Gitlab::Seeder::Pipelines
def job_attributes(pipeline, opts)
{ name: 'test build', stage: 'test', stage_idx: stage_index(opts[:stage]),
- ref: 'master', tag: false, user: build_user, project: @project, pipeline: pipeline,
+ ref: pipeline.ref, tag: false, user: build_user, project: @project, pipeline: pipeline,
created_at: Time.now, updated_at: Time.now
}.merge(opts)
end
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index e882a492757..916ee8dbac8 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -203,6 +203,8 @@ class Gitlab::Seeder::CycleAnalytics
pipeline.run!
Timecop.travel rand(1..6).hours.from_now
pipeline.succeed!
+
+ PipelineMetricsWorker.new.perform(pipeline.id)
end
end
diff --git a/db/fixtures/production/010_settings.rb b/db/fixtures/production/010_settings.rb
new file mode 100644
index 00000000000..5522f31629a
--- /dev/null
+++ b/db/fixtures/production/010_settings.rb
@@ -0,0 +1,16 @@
+if ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN'].present?
+ settings = ApplicationSetting.current || ApplicationSetting.create_from_defaults
+ settings.set_runners_registration_token(ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN'])
+
+ if settings.save
+ puts "Saved Runner Registration Token".color(:green)
+ else
+ puts "Could not save Runner Registration Token".color(:red)
+ puts
+ settings.errors.full_messages.map do |message|
+ puts "--> #{message}".color(:red)
+ end
+ puts
+ exit 1
+ end
+end
diff --git a/db/fixtures/test/001_repo.rb b/db/fixtures/test/001_repo.rb
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/db/fixtures/test/001_repo.rb
+++ /dev/null
diff --git a/db/migrate/20130319214458_create_forked_project_links.rb b/db/migrate/20130319214458_create_forked_project_links.rb
index 66eb11a4b2b..065a5e08243 100644
--- a/db/migrate/20130319214458_create_forked_project_links.rb
+++ b/db/migrate/20130319214458_create_forked_project_links.rb
@@ -1,11 +1,13 @@
# rubocop:disable all
class CreateForkedProjectLinks < ActiveRecord::Migration
+ DOWNTIME = false
+
def change
create_table :forked_project_links do |t|
t.integer :forked_to_project_id, null: false
t.integer :forked_from_project_id, null: false
- t.timestamps
+ t.timestamps null: true
end
add_index :forked_project_links, :forked_to_project_id, unique: true
end
diff --git a/db/migrate/20130506090604_create_deploy_keys_projects.rb b/db/migrate/20130506090604_create_deploy_keys_projects.rb
index 7d6662d358a..8b9662a27c3 100644
--- a/db/migrate/20130506090604_create_deploy_keys_projects.rb
+++ b/db/migrate/20130506090604_create_deploy_keys_projects.rb
@@ -1,11 +1,13 @@
# rubocop:disable all
class CreateDeployKeysProjects < ActiveRecord::Migration
+ DOWNTIME = false
+
def change
create_table :deploy_keys_projects do |t|
t.integer :deploy_key_id, null: false
t.integer :project_id, null: false
- t.timestamps
+ t.timestamps null: true
end
end
end
diff --git a/db/migrate/20130617095603_create_users_groups.rb b/db/migrate/20130617095603_create_users_groups.rb
index 45cff93fe4a..4ba7d0c9461 100644
--- a/db/migrate/20130617095603_create_users_groups.rb
+++ b/db/migrate/20130617095603_create_users_groups.rb
@@ -1,12 +1,14 @@
# rubocop:disable all
class CreateUsersGroups < ActiveRecord::Migration
+ DOWNTIME = false
+
def change
create_table :users_groups do |t|
t.integer :group_access, null: false
t.integer :group_id, null: false
t.integer :user_id, null: false
- t.timestamps
+ t.timestamps null: true
end
end
end
diff --git a/db/migrate/20130711063759_create_project_group_links.rb b/db/migrate/20130711063759_create_project_group_links.rb
index bd9d40a50db..efccb2aa938 100644
--- a/db/migrate/20130711063759_create_project_group_links.rb
+++ b/db/migrate/20130711063759_create_project_group_links.rb
@@ -1,11 +1,13 @@
# rubocop:disable all
class CreateProjectGroupLinks < ActiveRecord::Migration
+ DOWNTIME = false
+
def change
create_table :project_group_links do |t|
t.integer :project_id, null: false
t.integer :group_id, null: false
- t.timestamps
+ t.timestamps null: true
end
end
end
diff --git a/db/migrate/20131112114325_create_broadcast_messages.rb b/db/migrate/20131112114325_create_broadcast_messages.rb
index ce37a8e2708..ad2549e53af 100644
--- a/db/migrate/20131112114325_create_broadcast_messages.rb
+++ b/db/migrate/20131112114325_create_broadcast_messages.rb
@@ -1,5 +1,7 @@
# rubocop:disable all
class CreateBroadcastMessages < ActiveRecord::Migration
+ DOWNTIME = false
+
def change
create_table :broadcast_messages do |t|
t.text :message, null: false
@@ -7,7 +9,7 @@ class CreateBroadcastMessages < ActiveRecord::Migration
t.datetime :ends_at
t.integer :alert_type
- t.timestamps
+ t.timestamps null: true
end
end
end
diff --git a/db/migrate/20140122112253_create_merge_request_diffs.rb b/db/migrate/20140122112253_create_merge_request_diffs.rb
index 395c3edfc79..6c7a92b6950 100644
--- a/db/migrate/20140122112253_create_merge_request_diffs.rb
+++ b/db/migrate/20140122112253_create_merge_request_diffs.rb
@@ -1,5 +1,7 @@
# rubocop:disable all
class CreateMergeRequestDiffs < ActiveRecord::Migration
+ DOWNTIME = false
+
def up
create_table :merge_request_diffs do |t|
t.string :state, null: false, default: 'collected'
@@ -7,7 +9,7 @@ class CreateMergeRequestDiffs < ActiveRecord::Migration
t.text :st_diffs, null: true
t.integer :merge_request_id, null: false
- t.timestamps
+ t.timestamps null: true
end
if ActiveRecord::Base.configurations[Rails.env]['adapter'] =~ /^mysql/
diff --git a/db/migrate/20140209025651_create_emails.rb b/db/migrate/20140209025651_create_emails.rb
index 571beb19cdd..51886f8fc89 100644
--- a/db/migrate/20140209025651_create_emails.rb
+++ b/db/migrate/20140209025651_create_emails.rb
@@ -1,11 +1,13 @@
# rubocop:disable all
class CreateEmails < ActiveRecord::Migration
+ DOWNTIME = false
+
def change
create_table :emails do |t|
t.integer :user_id, null: false
t.string :email, null: false
-
- t.timestamps
+
+ t.timestamps null: true
end
add_index :emails, :user_id
diff --git a/db/migrate/20140625115202_create_users_star_projects.rb b/db/migrate/20140625115202_create_users_star_projects.rb
index 32dd99e83be..d4f3fe5ac62 100644
--- a/db/migrate/20140625115202_create_users_star_projects.rb
+++ b/db/migrate/20140625115202_create_users_star_projects.rb
@@ -1,10 +1,12 @@
# rubocop:disable all
class CreateUsersStarProjects < ActiveRecord::Migration
+ DOWNTIME = false
+
def change
create_table :users_star_projects do |t|
t.integer :project_id, null: false
t.integer :user_id, null: false
- t.timestamps
+ t.timestamps null: true
end
add_index :users_star_projects, :user_id
add_index :users_star_projects, :project_id
diff --git a/db/migrate/20140729134820_create_labels.rb b/db/migrate/20140729134820_create_labels.rb
index df0f8cb9f03..66d20e741a6 100644
--- a/db/migrate/20140729134820_create_labels.rb
+++ b/db/migrate/20140729134820_create_labels.rb
@@ -1,12 +1,14 @@
# rubocop:disable all
class CreateLabels < ActiveRecord::Migration
+ DOWNTIME = false
+
def change
create_table :labels do |t|
t.string :title
t.string :color
t.integer :project_id
- t.timestamps
+ t.timestamps null: true
end
end
end
diff --git a/db/migrate/20140729140420_create_label_links.rb b/db/migrate/20140729140420_create_label_links.rb
index fa5992605f8..dacd9f2e4b6 100644
--- a/db/migrate/20140729140420_create_label_links.rb
+++ b/db/migrate/20140729140420_create_label_links.rb
@@ -1,12 +1,14 @@
# rubocop:disable all
class CreateLabelLinks < ActiveRecord::Migration
+ DOWNTIME = false
+
def change
create_table :label_links do |t|
t.integer :label_id
t.integer :target_id
t.string :target_type
- t.timestamps
+ t.timestamps null: true
end
end
end
diff --git a/db/migrate/20140914113604_add_members_table.rb b/db/migrate/20140914113604_add_members_table.rb
index bc3c1bb61e4..0f76bb0ef79 100644
--- a/db/migrate/20140914113604_add_members_table.rb
+++ b/db/migrate/20140914113604_add_members_table.rb
@@ -1,5 +1,7 @@
# rubocop:disable all
class AddMembersTable < ActiveRecord::Migration
+ DOWNTIME = false
+
def change
create_table :members do |t|
t.integer :access_level, null: false
@@ -9,7 +11,7 @@ class AddMembersTable < ActiveRecord::Migration
t.integer :notification_level, null: false
t.string :type
- t.timestamps
+ t.timestamps null: true
end
add_index :members, :type
diff --git a/db/migrate/20140914173417_remove_old_member_tables.rb b/db/migrate/20140914173417_remove_old_member_tables.rb
index aff8e94e5be..d2ab326ef1f 100644
--- a/db/migrate/20140914173417_remove_old_member_tables.rb
+++ b/db/migrate/20140914173417_remove_old_member_tables.rb
@@ -1,5 +1,7 @@
# rubocop:disable all
class RemoveOldMemberTables < ActiveRecord::Migration
+ DOWNTIME = false
+
def up
drop_table :users_groups
drop_table :users_projects
@@ -12,7 +14,7 @@ class RemoveOldMemberTables < ActiveRecord::Migration
t.integer :user_id, null: false
t.integer :notification_level, null: false, default: 3
- t.timestamps
+ t.timestamps null: true
end
create_table :users_projects do |t|
@@ -21,7 +23,7 @@ class RemoveOldMemberTables < ActiveRecord::Migration
t.integer :user_id, null: false
t.integer :notification_level, null: false, default: 3
- t.timestamps
+ t.timestamps null: true
end
end
end
diff --git a/db/migrate/20141006143943_move_slack_service_to_webhook.rb b/db/migrate/20141006143943_move_slack_service_to_webhook.rb
index 8cb120f7007..42e88d6d6e3 100644
--- a/db/migrate/20141006143943_move_slack_service_to_webhook.rb
+++ b/db/migrate/20141006143943_move_slack_service_to_webhook.rb
@@ -1,7 +1,11 @@
# rubocop:disable all
class MoveSlackServiceToWebhook < ActiveRecord::Migration
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Move old fields "token" and "subdomain" to one single field "webhook"'
+
def change
- SlackService.all.each do |slack_service|
+ SlackNotificationService.all.each do |slack_service|
if ["token", "subdomain"].all? { |property| slack_service.properties.key? property }
token = slack_service.properties['token']
subdomain = slack_service.properties['subdomain']
diff --git a/db/migrate/20141118150935_add_audit_event.rb b/db/migrate/20141118150935_add_audit_event.rb
index 3884228456f..52d70b4a0ac 100644
--- a/db/migrate/20141118150935_add_audit_event.rb
+++ b/db/migrate/20141118150935_add_audit_event.rb
@@ -1,5 +1,7 @@
# rubocop:disable all
class AddAuditEvent < ActiveRecord::Migration
+ DOWNTIME = false
+
def change
create_table :audit_events do |t|
t.integer :author_id, null: false
@@ -13,7 +15,7 @@ class AddAuditEvent < ActiveRecord::Migration
# Details for the event
t.text :details
- t.timestamps
+ t.timestamps null: true
end
add_index :audit_events, :author_id
diff --git a/db/migrate/20141216155758_create_doorkeeper_tables.rb b/db/migrate/20141216155758_create_doorkeeper_tables.rb
index b323ffe96f5..17e45a77291 100644
--- a/db/migrate/20141216155758_create_doorkeeper_tables.rb
+++ b/db/migrate/20141216155758_create_doorkeeper_tables.rb
@@ -1,5 +1,7 @@
# rubocop:disable all
class CreateDoorkeeperTables < ActiveRecord::Migration
+ DOWNTIME = false
+
def change
create_table :oauth_applications do |t|
t.string :name, null: false
@@ -7,7 +9,7 @@ class CreateDoorkeeperTables < ActiveRecord::Migration
t.string :secret, null: false
t.text :redirect_uri, null: false
t.string :scopes, null: false, default: ''
- t.timestamps
+ t.timestamps null: true
end
add_index :oauth_applications, :uid, unique: true
diff --git a/db/migrate/20150108073740_create_application_settings.rb b/db/migrate/20150108073740_create_application_settings.rb
index dfa2f765357..0e4c66ca8c0 100644
--- a/db/migrate/20150108073740_create_application_settings.rb
+++ b/db/migrate/20150108073740_create_application_settings.rb
@@ -1,5 +1,7 @@
# rubocop:disable all
class CreateApplicationSettings < ActiveRecord::Migration
+ DOWNTIME = false
+
def change
create_table :application_settings do |t|
t.integer :default_projects_limit
@@ -8,7 +10,7 @@ class CreateApplicationSettings < ActiveRecord::Migration
t.boolean :gravatar_enabled
t.text :sign_in_text
- t.timestamps
+ t.timestamps null: true
end
end
end
diff --git a/db/migrate/20150313012111_create_subscriptions_table.rb b/db/migrate/20150313012111_create_subscriptions_table.rb
index 8adb193b27f..a9a8435330d 100644
--- a/db/migrate/20150313012111_create_subscriptions_table.rb
+++ b/db/migrate/20150313012111_create_subscriptions_table.rb
@@ -1,15 +1,17 @@
# rubocop:disable all
class CreateSubscriptionsTable < ActiveRecord::Migration
+ DOWNTIME = false
+
def change
create_table :subscriptions do |t|
t.integer :user_id
t.references :subscribable, polymorphic: true
t.boolean :subscribed
-
- t.timestamps
+
+ t.timestamps null: true
end
- add_index :subscriptions,
+ add_index :subscriptions,
[:subscribable_id, :subscribable_type, :user_id],
unique: true,
name: 'subscriptions_user_id_and_ref_fields'
diff --git a/db/migrate/20150806104937_create_abuse_reports.rb b/db/migrate/20150806104937_create_abuse_reports.rb
index 3c749b5d9a9..52aed9e1d1d 100644
--- a/db/migrate/20150806104937_create_abuse_reports.rb
+++ b/db/migrate/20150806104937_create_abuse_reports.rb
@@ -1,12 +1,14 @@
# rubocop:disable all
class CreateAbuseReports < ActiveRecord::Migration
+ DOWNTIME = false
+
def change
create_table :abuse_reports do |t|
t.integer :reporter_id
t.integer :user_id
t.text :message
- t.timestamps
+ t.timestamps null: true
end
end
end
diff --git a/db/migrate/20151103134857_create_lfs_objects.rb b/db/migrate/20151103134857_create_lfs_objects.rb
index 745b52e2b24..db6fa27199b 100644
--- a/db/migrate/20151103134857_create_lfs_objects.rb
+++ b/db/migrate/20151103134857_create_lfs_objects.rb
@@ -1,11 +1,13 @@
# rubocop:disable all
class CreateLfsObjects < ActiveRecord::Migration
+ DOWNTIME = false
+
def change
create_table :lfs_objects do |t|
t.string :oid, null: false, unique: true
t.integer :size, null: false
- t.timestamps
+ t.timestamps null: true
end
end
end
diff --git a/db/migrate/20151103134958_create_lfs_objects_projects.rb b/db/migrate/20151103134958_create_lfs_objects_projects.rb
index 3178e85b899..5af1c39fd9c 100644
--- a/db/migrate/20151103134958_create_lfs_objects_projects.rb
+++ b/db/migrate/20151103134958_create_lfs_objects_projects.rb
@@ -1,11 +1,13 @@
# rubocop:disable all
class CreateLfsObjectsProjects < ActiveRecord::Migration
+ DOWNTIME = false
+
def change
create_table :lfs_objects_projects do |t|
t.integer :lfs_object_id, null: false
t.integer :project_id, null: false
- t.timestamps
+ t.timestamps null: true
end
add_index :lfs_objects_projects, :project_id
diff --git a/db/migrate/20151105094515_create_releases.rb b/db/migrate/20151105094515_create_releases.rb
index 145b8db1486..34dd7a10942 100644
--- a/db/migrate/20151105094515_create_releases.rb
+++ b/db/migrate/20151105094515_create_releases.rb
@@ -1,12 +1,14 @@
# rubocop:disable all
class CreateReleases < ActiveRecord::Migration
+ DOWNTIME = false
+
def change
create_table :releases do |t|
t.string :tag
t.text :description
t.integer :project_id
- t.timestamps
+ t.timestamps null: true
end
add_index :releases, :project_id
diff --git a/db/migrate/20160212123307_create_tasks.rb b/db/migrate/20160212123307_create_tasks.rb
index 20573b01351..cd3ad0e4cd8 100644
--- a/db/migrate/20160212123307_create_tasks.rb
+++ b/db/migrate/20160212123307_create_tasks.rb
@@ -1,5 +1,7 @@
# rubocop:disable all
class CreateTasks < ActiveRecord::Migration
+ DOWNTIME = false
+
def change
create_table :tasks do |t|
t.references :user, null: false, index: true
@@ -9,7 +11,7 @@ class CreateTasks < ActiveRecord::Migration
t.integer :action, null: false
t.string :state, null: false, index: true
- t.timestamps
+ t.timestamps null: true
end
end
end
diff --git a/db/migrate/20160416180807_add_award_emoji.rb b/db/migrate/20160416180807_add_award_emoji.rb
index a3bee9b1bc6..0d252e5044e 100644
--- a/db/migrate/20160416180807_add_award_emoji.rb
+++ b/db/migrate/20160416180807_add_award_emoji.rb
@@ -1,12 +1,14 @@
# rubocop:disable all
class AddAwardEmoji < ActiveRecord::Migration
+ DOWNTIME = false
+
def change
create_table :award_emoji do |t|
t.string :name
t.references :user
t.references :awardable, polymorphic: true
- t.timestamps
+ t.timestamps null: true
end
add_index :award_emoji, :user_id
diff --git a/db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb b/db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb
new file mode 100644
index 00000000000..91479de840b
--- /dev/null
+++ b/db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb
@@ -0,0 +1,19 @@
+# The default needs to be `[]`, but all existing access tokens need to have `scopes` set to `['api']`.
+# It's easier to achieve this by adding the column with the `['api']` default, and then changing the default to
+# `[]`.
+
+class AddColumnScopesToPersonalAccessTokens < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :personal_access_tokens, :scopes, :string, default: ['api'].to_yaml
+ end
+
+ def down
+ remove_column :personal_access_tokens, :scopes
+ end
+end
diff --git a/db/migrate/20160831214002_create_project_features.rb b/db/migrate/20160831214002_create_project_features.rb
index 2d76a015a08..343953826f0 100644
--- a/db/migrate/20160831214002_create_project_features.rb
+++ b/db/migrate/20160831214002_create_project_features.rb
@@ -10,7 +10,7 @@ class CreateProjectFeatures < ActiveRecord::Migration
t.integer :snippets_access_level
t.integer :builds_access_level
- t.timestamps
+ t.timestamps null: true
end
end
end
diff --git a/db/migrate/20160914131004_only_allow_merge_if_all_discussions_are_resolved.rb b/db/migrate/20160914131004_only_allow_merge_if_all_discussions_are_resolved.rb
index fad62d716b3..4da5ec9bd28 100644
--- a/db/migrate/20160914131004_only_allow_merge_if_all_discussions_are_resolved.rb
+++ b/db/migrate/20160914131004_only_allow_merge_if_all_discussions_are_resolved.rb
@@ -5,10 +5,7 @@ class OnlyAllowMergeIfAllDiscussionsAreResolved < ActiveRecord::Migration
disable_ddl_transaction!
def up
- add_column_with_default(:projects,
- :only_allow_merge_if_all_discussions_are_resolved,
- :boolean,
- default: false)
+ add_column :projects, :only_allow_merge_if_all_discussions_are_resolved, :boolean
end
def down
diff --git a/db/migrate/20161010142410_create_project_authorizations.rb b/db/migrate/20161010142410_create_project_authorizations.rb
new file mode 100644
index 00000000000..e095ab969f8
--- /dev/null
+++ b/db/migrate/20161010142410_create_project_authorizations.rb
@@ -0,0 +1,15 @@
+class CreateProjectAuthorizations < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :project_authorizations do |t|
+ t.references :user, foreign_key: { on_delete: :cascade }
+ t.references :project, foreign_key: { on_delete: :cascade }
+ t.integer :access_level
+
+ t.index [:user_id, :project_id, :access_level], unique: true, name: 'index_project_authorizations_on_user_id_project_id_access_level'
+ end
+ end
+end
diff --git a/db/migrate/20161017091941_add_authorized_projects_populated_to_users.rb b/db/migrate/20161017091941_add_authorized_projects_populated_to_users.rb
new file mode 100644
index 00000000000..8f6be9dd677
--- /dev/null
+++ b/db/migrate/20161017091941_add_authorized_projects_populated_to_users.rb
@@ -0,0 +1,9 @@
+class AddAuthorizedProjectsPopulatedToUsers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :users, :authorized_projects_populated, :boolean
+ end
+end
diff --git a/db/migrate/20161020075734_default_request_access_groups.rb b/db/migrate/20161020075734_default_request_access_groups.rb
new file mode 100644
index 00000000000..9721cc88724
--- /dev/null
+++ b/db/migrate/20161020075734_default_request_access_groups.rb
@@ -0,0 +1,12 @@
+class DefaultRequestAccessGroups < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ def up
+ change_column_default :namespaces, :request_access_enabled, false
+ end
+
+ def down
+ change_column_default :namespaces, :request_access_enabled, true
+ end
+end
diff --git a/db/migrate/20161020075830_default_request_access_projects.rb b/db/migrate/20161020075830_default_request_access_projects.rb
new file mode 100644
index 00000000000..cb790291b24
--- /dev/null
+++ b/db/migrate/20161020075830_default_request_access_projects.rb
@@ -0,0 +1,12 @@
+class DefaultRequestAccessProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ def up
+ change_column_default :projects, :request_access_enabled, false
+ end
+
+ def down
+ change_column_default :projects, :request_access_enabled, true
+ end
+end
diff --git a/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb
new file mode 100644
index 00000000000..f49df6802a7
--- /dev/null
+++ b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb
@@ -0,0 +1,33 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPipelineIdToMergeRequestMetrics < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = true
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ DOWNTIME_REASON = 'Adding a foreign key'
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ add_column :merge_request_metrics, :pipeline_id, :integer
+ add_concurrent_index :merge_request_metrics, :pipeline_id
+ add_foreign_key :merge_request_metrics, :ci_commits, column: :pipeline_id, on_delete: :cascade
+ end
+end
diff --git a/db/migrate/20161031171301_add_project_id_to_subscriptions.rb b/db/migrate/20161031171301_add_project_id_to_subscriptions.rb
new file mode 100644
index 00000000000..97534679b59
--- /dev/null
+++ b/db/migrate/20161031171301_add_project_id_to_subscriptions.rb
@@ -0,0 +1,14 @@
+class AddProjectIdToSubscriptions < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ add_column :subscriptions, :project_id, :integer
+ add_foreign_key :subscriptions, :projects, column: :project_id, on_delete: :cascade
+ end
+
+ def down
+ remove_column :subscriptions, :project_id
+ end
+end
diff --git a/db/migrate/20161031174110_migrate_subscriptions_project_id.rb b/db/migrate/20161031174110_migrate_subscriptions_project_id.rb
new file mode 100644
index 00000000000..549145a0a65
--- /dev/null
+++ b/db/migrate/20161031174110_migrate_subscriptions_project_id.rb
@@ -0,0 +1,44 @@
+class MigrateSubscriptionsProjectId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Subscriptions will not work as expected until this migration is complete.'
+
+ def up
+ execute <<-EOF.strip_heredoc
+ UPDATE subscriptions
+ SET project_id = (
+ SELECT issues.project_id
+ FROM issues
+ WHERE issues.id = subscriptions.subscribable_id
+ )
+ WHERE subscriptions.subscribable_type = 'Issue';
+ EOF
+
+ execute <<-EOF.strip_heredoc
+ UPDATE subscriptions
+ SET project_id = (
+ SELECT merge_requests.target_project_id
+ FROM merge_requests
+ WHERE merge_requests.id = subscriptions.subscribable_id
+ )
+ WHERE subscriptions.subscribable_type = 'MergeRequest';
+ EOF
+
+ execute <<-EOF.strip_heredoc
+ UPDATE subscriptions
+ SET project_id = (
+ SELECT projects.id
+ FROM labels INNER JOIN projects ON projects.id = labels.project_id
+ WHERE labels.id = subscriptions.subscribable_id
+ )
+ WHERE subscriptions.subscribable_type = 'Label';
+ EOF
+ end
+
+ def down
+ execute <<-EOF.strip_heredoc
+ UPDATE subscriptions SET project_id = NULL;
+ EOF
+ end
+end
diff --git a/db/migrate/20161031181638_add_unique_index_to_subscriptions.rb b/db/migrate/20161031181638_add_unique_index_to_subscriptions.rb
new file mode 100644
index 00000000000..4b1b29e1265
--- /dev/null
+++ b/db/migrate/20161031181638_add_unique_index_to_subscriptions.rb
@@ -0,0 +1,18 @@
+class AddUniqueIndexToSubscriptions < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'This migration requires downtime because it changes a column to not accept null values.'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :subscriptions, [:subscribable_id, :subscribable_type, :user_id, :project_id], { unique: true, name: 'index_subscriptions_on_subscribable_and_user_id_and_project_id' }
+ remove_index :subscriptions, name: 'subscriptions_user_id_and_ref_fields' if index_name_exists?(:subscriptions, 'subscriptions_user_id_and_ref_fields', false)
+ end
+
+ def down
+ add_concurrent_index :subscriptions, [:subscribable_id, :subscribable_type, :user_id], { unique: true, name: 'subscriptions_user_id_and_ref_fields' }
+ remove_index :subscriptions, name: 'index_subscriptions_on_subscribable_and_user_id_and_project_id' if index_name_exists?(:subscriptions, 'index_subscriptions_on_subscribable_and_user_id_and_project_id', false)
+ end
+end
diff --git a/db/migrate/20161103191444_add_sidekiq_throttling_to_application_settings.rb b/db/migrate/20161103191444_add_sidekiq_throttling_to_application_settings.rb
new file mode 100644
index 00000000000..e644a174964
--- /dev/null
+++ b/db/migrate/20161103191444_add_sidekiq_throttling_to_application_settings.rb
@@ -0,0 +1,31 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddSidekiqThrottlingToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ add_column :application_settings, :sidekiq_throttling_enabled, :boolean, default: false
+ add_column :application_settings, :sidekiq_throttling_queues, :string
+ add_column :application_settings, :sidekiq_throttling_factor, :decimal
+ end
+end
diff --git a/db/migrate/20161113184239_create_user_chat_names_table.rb b/db/migrate/20161113184239_create_user_chat_names_table.rb
new file mode 100644
index 00000000000..97b597654f7
--- /dev/null
+++ b/db/migrate/20161113184239_create_user_chat_names_table.rb
@@ -0,0 +1,21 @@
+class CreateUserChatNamesTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :chat_names do |t|
+ t.integer :user_id, null: false
+ t.integer :service_id, null: false
+ t.string :team_id, null: false
+ t.string :team_domain
+ t.string :chat_id, null: false
+ t.string :chat_name
+ t.datetime :last_used_at
+ t.timestamps null: false
+ end
+
+ add_index :chat_names, [:user_id, :service_id], unique: true
+ add_index :chat_names, [:service_id, :team_id, :chat_id], unique: true
+ end
+end
diff --git a/db/migrate/20161115173905_add_start_date_to_milestones.rb b/db/migrate/20161115173905_add_start_date_to_milestones.rb
new file mode 100644
index 00000000000..413733b8db7
--- /dev/null
+++ b/db/migrate/20161115173905_add_start_date_to_milestones.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 AddStartDateToMilestones < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :milestones, :start_date, :date
+ end
+end
diff --git a/db/migrate/20161117114805_remove_undeleted_groups.rb b/db/migrate/20161117114805_remove_undeleted_groups.rb
new file mode 100644
index 00000000000..696914f8e4d
--- /dev/null
+++ b/db/migrate/20161117114805_remove_undeleted_groups.rb
@@ -0,0 +1,57 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveUndeletedGroups < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ execute <<-EOF.strip_heredoc
+ DELETE FROM projects
+ WHERE namespace_id IN (
+ SELECT id FROM (
+ SELECT id
+ FROM namespaces
+ WHERE deleted_at IS NOT NULL
+ ) namespace_ids
+ );
+ EOF
+
+ if defined?(Gitlab::License)
+ # EE adds these columns but we have to make sure this data is cleaned up
+ # here before we run the DELETE below. An alternative would be patching
+ # this migration in EE but this will only result in a mess and confusing
+ # migrations.
+ execute <<-EOF.strip_heredoc
+ DELETE FROM protected_branch_push_access_levels
+ WHERE group_id IN (
+ SELECT id FROM (
+ SELECT id
+ FROM namespaces
+ WHERE deleted_at IS NOT NULL
+ ) namespace_ids
+ );
+ EOF
+
+ execute <<-EOF.strip_heredoc
+ DELETE FROM protected_branch_merge_access_levels
+ WHERE group_id IN (
+ SELECT id FROM (
+ SELECT id
+ FROM namespaces
+ WHERE deleted_at IS NOT NULL
+ ) namespace_ids
+ );
+ EOF
+ end
+
+ # This removes namespaces that were supposed to be soft deleted but still
+ # reside in the database.
+ execute "DELETE FROM namespaces WHERE deleted_at IS NOT NULL;"
+ end
+
+ def down
+ # This is an irreversible migration;
+ # If someone is trying to rollback for other reasons, we should not throw an Exception.
+ # raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/db/migrate/20161118183841_add_commit_events_to_services.rb b/db/migrate/20161118183841_add_commit_events_to_services.rb
new file mode 100644
index 00000000000..4f9b5dd2281
--- /dev/null
+++ b/db/migrate/20161118183841_add_commit_events_to_services.rb
@@ -0,0 +1,15 @@
+class AddCommitEventsToServices < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:services, :commit_events, :boolean, default: true, allow_null: false)
+ end
+
+ def down
+ remove_column(:services, :commit_events)
+ end
+end
diff --git a/db/migrate/20161124111390_add_parent_id_to_namespace.rb b/db/migrate/20161124111390_add_parent_id_to_namespace.rb
new file mode 100644
index 00000000000..a6fa1b70a9d
--- /dev/null
+++ b/db/migrate/20161124111390_add_parent_id_to_namespace.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 AddParentIdToNamespace < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column(:namespaces, :parent_id, :integer)
+ end
+end
diff --git a/db/migrate/20161124111395_add_index_to_parent_id.rb b/db/migrate/20161124111395_add_index_to_parent_id.rb
new file mode 100644
index 00000000000..eab74c01dfd
--- /dev/null
+++ b/db/migrate/20161124111395_add_index_to_parent_id.rb
@@ -0,0 +1,14 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexToParentId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index(:namespaces, [:parent_id, :id], unique: true)
+ end
+end
diff --git a/db/migrate/20161124111402_add_routes_table.rb b/db/migrate/20161124111402_add_routes_table.rb
new file mode 100644
index 00000000000..a02e046a18e
--- /dev/null
+++ b/db/migrate/20161124111402_add_routes_table.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddRoutesTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :routes do |t|
+ t.integer :source_id, null: false
+ t.string :source_type, null: false
+ t.string :path, null: false
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
new file mode 100644
index 00000000000..77e0c40d850
--- /dev/null
+++ b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
@@ -0,0 +1,102 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ class Project < ActiveRecord::Base
+ def self.find_including_path(id)
+ select("projects.*, CONCAT(namespaces.path, '/', projects.path) AS path_with_namespace").
+ joins('INNER JOIN namespaces ON namespaces.id = projects.namespace_id').
+ find_by(id: id)
+ end
+
+ def repository_storage_path
+ Gitlab.config.repositories.storages[repository_storage]
+ end
+
+ def repository_path
+ File.join(repository_storage_path, read_attribute(:path_with_namespace) + '.git')
+ end
+
+ def repository
+ @repository ||= Rugged::Repository.new(repository_path)
+ end
+ end
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Existing workers will error until they are using a newer version of the code'
+
+ disable_ddl_transaction!
+
+ def up
+ Sidekiq.redis do |redis|
+ new_jobs = []
+
+ while job = redis.lpop('queue:process_commit')
+ payload = JSON.load(job)
+ project = Project.find_including_path(payload['args'][0])
+
+ next unless project
+
+ begin
+ commit = project.repository.lookup(payload['args'][2])
+ rescue Rugged::OdbError
+ next
+ end
+
+ hash = {
+ id: commit.oid,
+ message: encode(commit.message),
+ parent_ids: commit.parent_ids,
+ authored_date: commit.author[:time],
+ author_name: encode(commit.author[:name]),
+ author_email: encode(commit.author[:email]),
+ committed_date: commit.committer[:time],
+ committer_email: encode(commit.committer[:email]),
+ committer_name: encode(commit.committer[:name])
+ }
+
+ payload['args'][2] = hash
+
+ new_jobs << JSON.dump(payload)
+ end
+
+ redis.multi do |multi|
+ new_jobs.each do |j|
+ multi.lpush('queue:process_commit', j)
+ end
+ end
+ end
+ end
+
+ def down
+ Sidekiq.redis do |redis|
+ new_jobs = []
+
+ while job = redis.lpop('queue:process_commit')
+ payload = JSON.load(job)
+
+ payload['args'][2] = payload['args'][2]['id']
+
+ new_jobs << JSON.dump(payload)
+ end
+
+ redis.multi do |multi|
+ new_jobs.each do |j|
+ multi.lpush('queue:process_commit', j)
+ end
+ end
+ end
+ end
+
+ def encode(data)
+ encoding = Encoding::UTF_8
+
+ if data.encoding == encoding
+ data
+ else
+ data.encode(encoding, invalid: :replace, undef: :replace)
+ end
+ end
+end
diff --git a/db/migrate/20161128142110_remove_unnecessary_indexes.rb b/db/migrate/20161128142110_remove_unnecessary_indexes.rb
new file mode 100644
index 00000000000..9deab19782e
--- /dev/null
+++ b/db/migrate/20161128142110_remove_unnecessary_indexes.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 RemoveUnnecessaryIndexes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ remove_index :labels, column: :group_id if index_exists?(:labels, :group_id)
+ remove_index :award_emoji, column: :user_id if index_exists?(:award_emoji, :user_id)
+ remove_index :ci_builds, column: :commit_id if index_exists?(:ci_builds, :commit_id)
+ remove_index :deployments, column: :project_id if index_exists?(:deployments, :project_id)
+ remove_index :deployments, column: ["project_id", "environment_id"] if index_exists?(:deployments, ["project_id", "environment_id"])
+ remove_index :lists, column: :board_id if index_exists?(:lists, :board_id)
+ remove_index :milestones, column: :project_id if index_exists?(:milestones, :project_id)
+ remove_index :notes, column: :project_id if index_exists?(:notes, :project_id)
+ remove_index :users_star_projects, column: :user_id if index_exists?(:users_star_projects, :user_id)
+ end
+
+ def down
+ add_concurrent_index :labels, :group_id
+ add_concurrent_index :award_emoji, :user_id
+ add_concurrent_index :ci_builds, :commit_id
+ add_concurrent_index :deployments, :project_id
+ add_concurrent_index :deployments, ["project_id", "environment_id"]
+ add_concurrent_index :lists, :board_id
+ add_concurrent_index :milestones, :project_id
+ add_concurrent_index :notes, :project_id
+ add_concurrent_index :users_star_projects, :user_id
+ end
+end
diff --git a/db/migrate/20161128161412_add_html_emails_enabled_to_application_settings.rb b/db/migrate/20161128161412_add_html_emails_enabled_to_application_settings.rb
new file mode 100644
index 00000000000..1c59241d0fe
--- /dev/null
+++ b/db/migrate/20161128161412_add_html_emails_enabled_to_application_settings.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddHtmlEmailsEnabledToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ add_column :application_settings, :html_emails_enabled, :boolean, default: true
+ end
+end
diff --git a/db/migrate/20161130095245_fill_routes_table.rb b/db/migrate/20161130095245_fill_routes_table.rb
new file mode 100644
index 00000000000..c3536d6d911
--- /dev/null
+++ b/db/migrate/20161130095245_fill_routes_table.rb
@@ -0,0 +1,21 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class FillRoutesTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'No new namespaces should be created during data copy'
+
+ def up
+ execute <<-EOF
+ INSERT INTO routes
+ (source_id, source_type, path)
+ (SELECT id, 'Namespace', path FROM namespaces)
+ EOF
+ end
+
+ def down
+ execute("DELETE FROM routes WHERE source_type = 'Namespace'")
+ end
+end
diff --git a/db/migrate/20161130101252_fill_projects_routes_table.rb b/db/migrate/20161130101252_fill_projects_routes_table.rb
new file mode 100644
index 00000000000..56ba6fcdbe3
--- /dev/null
+++ b/db/migrate/20161130101252_fill_projects_routes_table.rb
@@ -0,0 +1,30 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class FillProjectsRoutesTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'No new projects should be created during data copy'
+
+ def up
+ if Gitlab::Database.postgresql?
+ execute <<-EOF
+ INSERT INTO routes (source_id, source_type, path)
+ (SELECT DISTINCT ON (namespaces.path, projects.path) projects.id, 'Project', concat(namespaces.path, '/', projects.path)
+ FROM projects INNER JOIN namespaces ON projects.namespace_id = namespaces.id
+ ORDER BY namespaces.path, projects.path, projects.id DESC)
+ EOF
+ else
+ execute <<-EOF
+ INSERT INTO routes (source_id, source_type, path)
+ (SELECT projects.id, 'Project', concat(namespaces.path, '/', projects.path)
+ FROM projects INNER JOIN namespaces ON projects.namespace_id = namespaces.id)
+ EOF
+ end
+ end
+
+ def down
+ execute("DELETE FROM routes WHERE source_type = 'Project'")
+ end
+end
diff --git a/db/migrate/20161202152031_remove_duplicates_from_routes.rb b/db/migrate/20161202152031_remove_duplicates_from_routes.rb
new file mode 100644
index 00000000000..d73b0847506
--- /dev/null
+++ b/db/migrate/20161202152031_remove_duplicates_from_routes.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveDuplicatesFromRoutes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ # We can skip this migration when running a PostgreSQL database because
+ # we use an optimized query in the "FillProjectsRoutesTable" migration
+ # to fill these values that avoid duplicate entries in the routes table.
+ return unless Gitlab::Database.mysql?
+
+ execute <<-EOF
+ DELETE duplicated_rows.*
+ FROM routes AS duplicated_rows
+ INNER JOIN (
+ SELECT path, MAX(id) as max_id
+ FROM routes
+ GROUP BY path
+ HAVING COUNT(*) > 1
+ ) AS good_rows ON good_rows.path = duplicated_rows.path AND good_rows.max_id <> duplicated_rows.id;
+ EOF
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20161202152035_add_index_to_routes.rb b/db/migrate/20161202152035_add_index_to_routes.rb
new file mode 100644
index 00000000000..4a51337bda6
--- /dev/null
+++ b/db/migrate/20161202152035_add_index_to_routes.rb
@@ -0,0 +1,16 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexToRoutes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index(:routes, :path, unique: true)
+ add_concurrent_index(:routes, [:source_type, :source_id], unique: true)
+ end
+end
diff --git a/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb b/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb
new file mode 100644
index 00000000000..2977917f2d1
--- /dev/null
+++ b/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb
@@ -0,0 +1,36 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveUniqPathIndexFromNamespace < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ constraint_name = 'namespaces_path_key'
+
+ transaction do
+ if index_exists?(:namespaces, :path)
+ remove_index(:namespaces, :path)
+ end
+
+ # In some bizarre cases PostgreSQL might have a separate unique constraint
+ # that we'll need to drop.
+ if constraint_exists?(constraint_name) && Gitlab::Database.postgresql?
+ execute("ALTER TABLE namespaces DROP CONSTRAINT IF EXISTS #{constraint_name};")
+ end
+ end
+ end
+
+ def down
+ unless index_exists?(:namespaces, :path)
+ add_concurrent_index(:namespaces, :path, unique: true)
+ end
+ end
+
+ def constraint_exists?(name)
+ indexes(:namespaces).map(&:name).include?(name)
+ end
+end
diff --git a/db/migrate/20161206153751_add_path_index_to_namespace.rb b/db/migrate/20161206153751_add_path_index_to_namespace.rb
new file mode 100644
index 00000000000..b0bac7d121e
--- /dev/null
+++ b/db/migrate/20161206153751_add_path_index_to_namespace.rb
@@ -0,0 +1,20 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPathIndexToNamespace < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_concurrent_index :namespaces, :path
+ end
+
+ def down
+ if index_exists?(:namespaces, :path)
+ remove_index :namespaces, :path
+ end
+ end
+end
diff --git a/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb b/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb
new file mode 100644
index 00000000000..cc9d4974baa
--- /dev/null
+++ b/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb
@@ -0,0 +1,36 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveUniqNameIndexFromNamespace < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ constraint_name = 'namespaces_name_key'
+
+ transaction do
+ if index_exists?(:namespaces, :name)
+ remove_index(:namespaces, :name)
+ end
+
+ # In some bizarre cases PostgreSQL might have a separate unique constraint
+ # that we'll need to drop.
+ if constraint_exists?(constraint_name) && Gitlab::Database.postgresql?
+ execute("ALTER TABLE namespaces DROP CONSTRAINT IF EXISTS #{constraint_name};")
+ end
+ end
+ end
+
+ def down
+ unless index_exists?(:namespaces, :name)
+ add_concurrent_index(:namespaces, :name, unique: true)
+ end
+ end
+
+ def constraint_exists?(name)
+ indexes(:namespaces).map(&:name).include?(name)
+ end
+end
diff --git a/db/migrate/20161206153754_add_name_index_to_namespace.rb b/db/migrate/20161206153754_add_name_index_to_namespace.rb
new file mode 100644
index 00000000000..b3f3cb68a99
--- /dev/null
+++ b/db/migrate/20161206153754_add_name_index_to_namespace.rb
@@ -0,0 +1,20 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddNameIndexToNamespace < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_concurrent_index(:namespaces, [:name, :parent_id], unique: true)
+ end
+
+ def down
+ if index_exists?(:namespaces, [:name, :parent_id])
+ remove_index :namespaces, [:name, :parent_id]
+ end
+ end
+end
diff --git a/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb
new file mode 100644
index 00000000000..b74552e762d
--- /dev/null
+++ b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb
@@ -0,0 +1,53 @@
+class FixupEnvironmentNameUniqueness < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Renaming non-unique environments'
+
+ def up
+ environments = Arel::Table.new(:environments)
+
+ # Get all [project_id, name] pairs that occur more than once
+ finder_sql = environments.
+ group(environments[:project_id], environments[:name]).
+ having(Arel.sql("COUNT(1)").gt(1)).
+ project(environments[:project_id], environments[:name]).
+ to_sql
+
+ conflicting = connection.exec_query(finder_sql)
+
+ conflicting.rows.each do |project_id, name|
+ fix_duplicates(project_id, name)
+ end
+ end
+
+ def down
+ # Nothing to do
+ end
+
+ # Rename conflicting environments by appending "-#{id}" to all but the first
+ def fix_duplicates(project_id, name)
+ environments = Arel::Table.new(:environments)
+ finder_sql = environments.
+ where(environments[:project_id].eq(project_id)).
+ where(environments[:name].eq(name)).
+ order(environments[:id].asc).
+ project(environments[:id], environments[:name]).
+ to_sql
+
+ # Now we have the data for all the conflicting rows
+ conflicts = connection.exec_query(finder_sql).rows
+ conflicts.shift # Leave the first row alone
+
+ conflicts.each do |id, name|
+ update_sql =
+ Arel::UpdateManager.new(ActiveRecord::Base).
+ table(environments).
+ set(environments[:name] => name + "-" + id.to_s).
+ where(environments[:id].eq(id)).
+ to_sql
+
+ connection.exec_update(update_sql, self.class.name, [])
+ end
+ end
+end
diff --git a/db/migrate/20161207231621_create_environment_name_unique_index.rb b/db/migrate/20161207231621_create_environment_name_unique_index.rb
new file mode 100644
index 00000000000..ac680c8d10f
--- /dev/null
+++ b/db/migrate/20161207231621_create_environment_name_unique_index.rb
@@ -0,0 +1,18 @@
+class CreateEnvironmentNameUniqueIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Making a non-unique index into a unique index'
+
+ def up
+ remove_index :environments, [:project_id, :name]
+ add_concurrent_index :environments, [:project_id, :name], unique: true
+ end
+
+ def down
+ remove_index :environments, [:project_id, :name], unique: true
+ add_concurrent_index :environments, [:project_id, :name]
+ end
+end
diff --git a/db/migrate/20161207231626_add_environment_slug.rb b/db/migrate/20161207231626_add_environment_slug.rb
new file mode 100644
index 00000000000..7153e6a32b1
--- /dev/null
+++ b/db/migrate/20161207231626_add_environment_slug.rb
@@ -0,0 +1,60 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddEnvironmentSlug < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Adding NOT NULL column environments.slug with dependent data'
+
+ # Used to generate random suffixes for the slug
+ NUMBERS = '0'..'9'
+ SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a
+
+ def up
+ environments = Arel::Table.new(:environments)
+
+ add_column :environments, :slug, :string
+ finder = environments.project(:id, :name)
+
+ connection.exec_query(finder.to_sql).rows.each do |id, name|
+ updater = Arel::UpdateManager.new(ActiveRecord::Base).
+ table(environments).
+ set(environments[:slug] => generate_slug(name)).
+ where(environments[:id].eq(id))
+
+ connection.exec_update(updater.to_sql, self.class.name, [])
+ end
+
+ change_column_null :environments, :slug, false
+ end
+
+ def down
+ remove_column :environments, :slug
+ end
+
+ # Copy of the Environment#generate_slug implementation
+ def generate_slug(name)
+ # Lowercase letters and numbers only
+ slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-')
+
+ # Must start with a letter
+ slugified = "env-" + slugified if NUMBERS.cover?(slugified[0])
+
+ # 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] == "-"
+
+ # 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
+
+ slugified
+ end
+
+ def random_suffix
+ (0..5).map { SUFFIX_CHARS.sample }.join
+ end
+end
diff --git a/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
new file mode 100644
index 00000000000..e9fcef1cd45
--- /dev/null
+++ b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
@@ -0,0 +1,15 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddUniqueIndexForEnvironmentSlug < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Adding a *unique* index to environments.slug'
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :environments, [:project_id, :slug], unique: true
+ end
+end
diff --git a/db/migrate/20161212142807_add_lower_path_index_to_routes.rb b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb
new file mode 100644
index 00000000000..6958500306f
--- /dev/null
+++ b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb
@@ -0,0 +1,22 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddLowerPathIndexToRoutes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ return unless Gitlab::Database.postgresql?
+
+ execute 'CREATE INDEX CONCURRENTLY index_on_routes_lower_path ON routes (LOWER(path));'
+ end
+
+ def down
+ return unless Gitlab::Database.postgresql?
+
+ remove_index :routes, name: :index_on_routes_lower_path
+ end
+end
diff --git a/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb b/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb
new file mode 100644
index 00000000000..a7278d7b5a6
--- /dev/null
+++ b/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb
@@ -0,0 +1,14 @@
+class ChangeSlackServiceToSlackNotificationService < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Rename SlackService to SlackNotificationService'
+
+ def up
+ execute("UPDATE services SET type = 'SlackNotificationService' WHERE type = 'SlackService'")
+ end
+
+ def down
+ execute("UPDATE services SET type = 'SlackService' WHERE type = 'SlackNotificationService'")
+ end
+end
diff --git a/db/post_migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb b/db/post_migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb
new file mode 100644
index 00000000000..7df561d82dd
--- /dev/null
+++ b/db/post_migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb
@@ -0,0 +1,19 @@
+# The default needs to be `[]`, but all existing access tokens need to have `scopes` set to `['api']`.
+# It's easier to achieve this by adding the column with the `['api']` default (regular migration), and
+# then changing the default to `[]` (in this post-migration).
+#
+# Details: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951#note_19721973
+
+class ChangePersonalAccessTokensDefaultBackToEmptyArray < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ change_column_default :personal_access_tokens, :scopes, [].to_yaml
+ end
+
+ def down
+ change_column_default :personal_access_tokens, :scopes, ['api'].to_yaml
+ end
+end
diff --git a/db/migrate/20161011222551_remove_inactive_jira_service_properties.rb b/db/post_migrate/20161011222551_remove_inactive_jira_service_properties.rb
index 319d86ac159..319d86ac159 100644
--- a/db/migrate/20161011222551_remove_inactive_jira_service_properties.rb
+++ b/db/post_migrate/20161011222551_remove_inactive_jira_service_properties.rb
diff --git a/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb b/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb
new file mode 100644
index 00000000000..df38591a333
--- /dev/null
+++ b/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb
@@ -0,0 +1,49 @@
+class FixProjectRecordsWithInvalidVisibility < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ BATCH_SIZE = 500
+ DOWNTIME = false
+
+ # This migration is idempotent and there's no sense in throwing away the
+ # partial result if it's interrupted
+ disable_ddl_transaction!
+
+ def up
+ projects = Arel::Table.new(:projects)
+ namespaces = Arel::Table.new(:namespaces)
+
+ finder_sql =
+ projects.
+ join(namespaces, Arel::Nodes::InnerJoin).
+ on(projects[:namespace_id].eq(namespaces[:id])).
+ where(projects[:visibility_level].gt(namespaces[:visibility_level])).
+ project(projects[:id], namespaces[:visibility_level]).
+ take(BATCH_SIZE).
+ to_sql
+
+ # Update matching rows in batches. Each batch can cause up to 3 UPDATE
+ # statements, in addition to the SELECT: one per visibility_level
+ loop do
+ to_update = connection.exec_query(finder_sql)
+ break if to_update.rows.count == 0
+
+ # row[0] is projects.id, row[1] is namespaces.visibility_level
+ updates = to_update.rows.each_with_object(Hash.new {|h, k| h[k] = [] }) do |row, obj|
+ obj[row[1]] << row[0]
+ end
+
+ updates.each do |visibility_level, project_ids|
+ updater = Arel::UpdateManager.new(ActiveRecord::Base).
+ table(projects).
+ set(projects[:visibility_level] => visibility_level).
+ where(projects[:id].in(project_ids))
+
+ ActiveRecord::Base.connection.exec_update(updater.to_sql, self.class.name, [])
+ end
+ end
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 62c325a52d7..14801b581e6 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20161106185620) do
+ActiveRecord::Schema.define(version: 20161213172958) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -98,11 +98,15 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.text "help_page_text_html"
t.text "shared_runners_text_html"
t.text "after_sign_up_text_html"
+ t.boolean "sidekiq_throttling_enabled", default: false
+ t.string "sidekiq_throttling_queues"
+ t.decimal "sidekiq_throttling_factor"
t.boolean "housekeeping_enabled", default: true, null: false
t.boolean "housekeeping_bitmaps_enabled", default: true, null: false
t.integer "housekeeping_incremental_repack_period", default: 10, null: false
t.integer "housekeeping_full_repack_period", default: 50, null: false
t.integer "housekeeping_gc_period", default: 200, null: false
+ t.boolean "html_emails_enabled", default: true
end
create_table "audit_events", force: :cascade do |t|
@@ -128,7 +132,6 @@ ActiveRecord::Schema.define(version: 20161106185620) do
add_index "award_emoji", ["awardable_type", "awardable_id"], name: "index_award_emoji_on_awardable_type_and_awardable_id", using: :btree
add_index "award_emoji", ["user_id", "name"], name: "index_award_emoji_on_user_id_and_name", using: :btree
- add_index "award_emoji", ["user_id"], name: "index_award_emoji_on_user_id", using: :btree
create_table "boards", force: :cascade do |t|
t.integer "project_id", null: false
@@ -149,6 +152,21 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.text "message_html"
end
+ create_table "chat_names", force: :cascade do |t|
+ t.integer "user_id", null: false
+ t.integer "service_id", null: false
+ t.string "team_id", null: false
+ t.string "team_domain"
+ t.string "chat_id", null: false
+ t.string "chat_name"
+ t.datetime "last_used_at"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "chat_names", ["service_id", "team_id", "chat_id"], name: "index_chat_names_on_service_id_and_team_id_and_chat_id", unique: true, using: :btree
+ add_index "chat_names", ["user_id", "service_id"], name: "index_chat_names_on_user_id_and_service_id", unique: true, using: :btree
+
create_table "ci_application_settings", force: :cascade do |t|
t.boolean "all_broken_builds"
t.boolean "add_pusher"
@@ -201,7 +219,6 @@ ActiveRecord::Schema.define(version: 20161106185620) do
add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree
add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree
add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
- add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree
add_index "ci_builds", ["gl_project_id"], name: "index_ci_builds_on_gl_project_id", using: :btree
add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
@@ -391,9 +408,7 @@ ActiveRecord::Schema.define(version: 20161106185620) do
end
add_index "deployments", ["project_id", "environment_id", "iid"], name: "index_deployments_on_project_id_and_environment_id_and_iid", using: :btree
- add_index "deployments", ["project_id", "environment_id"], name: "index_deployments_on_project_id_and_environment_id", using: :btree
add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree
- add_index "deployments", ["project_id"], name: "index_deployments_on_project_id", using: :btree
create_table "emails", force: :cascade do |t|
t.integer "user_id", null: false
@@ -413,9 +428,11 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.string "external_url"
t.string "environment_type"
t.string "state", default: "available", null: false
+ t.string "slug", null: false
end
- add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree
+ add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", unique: true, using: :btree
+ add_index "environments", ["project_id", "slug"], name: "index_environments_on_project_id_and_slug", unique: true, using: :btree
create_table "events", force: :cascade do |t|
t.string "target_type"
@@ -551,7 +568,6 @@ ActiveRecord::Schema.define(version: 20161106185620) 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", ["group_id"], name: "index_labels_on_group_id", using: :btree
create_table "lfs_objects", force: :cascade do |t|
t.string "oid", null: false
@@ -582,7 +598,6 @@ ActiveRecord::Schema.define(version: 20161106185620) do
end
add_index "lists", ["board_id", "label_id"], name: "index_lists_on_board_id_and_label_id", unique: true, using: :btree
- add_index "lists", ["board_id"], name: "index_lists_on_board_id", using: :btree
add_index "lists", ["label_id"], name: "index_lists_on_label_id", using: :btree
create_table "members", force: :cascade do |t|
@@ -631,10 +646,12 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.datetime "merged_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.integer "pipeline_id"
end
add_index "merge_request_metrics", ["first_deployed_to_production_at"], name: "index_merge_request_metrics_on_first_deployed_to_production_at", using: :btree
add_index "merge_request_metrics", ["merge_request_id"], name: "index_merge_request_metrics", using: :btree
+ add_index "merge_request_metrics", ["pipeline_id"], name: "index_merge_request_metrics_on_pipeline_id", using: :btree
create_table "merge_requests", force: :cascade do |t|
t.string "target_branch", null: false
@@ -700,12 +717,12 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.integer "iid"
t.text "title_html"
t.text "description_html"
+ t.date "start_date"
end
add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree
add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree
- add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree
add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree
add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
@@ -720,18 +737,20 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.string "avatar"
t.boolean "share_with_group_lock", default: false
t.integer "visibility_level", default: 20, null: false
- t.boolean "request_access_enabled", default: true, null: false
+ t.boolean "request_access_enabled", default: false, null: false
t.datetime "deleted_at"
t.boolean "lfs_enabled"
t.text "description_html"
+ t.integer "parent_id"
end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
add_index "namespaces", ["deleted_at"], name: "index_namespaces_on_deleted_at", using: :btree
- add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree
+ add_index "namespaces", ["name", "parent_id"], name: "index_namespaces_on_name_and_parent_id", unique: true, using: :btree
add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
- add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree
+ add_index "namespaces", ["parent_id", "id"], name: "index_namespaces_on_parent_id_and_id", unique: true, using: :btree
+ add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree
add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
@@ -768,7 +787,6 @@ ActiveRecord::Schema.define(version: 20161106185620) do
add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree
add_index "notes", ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree
- add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree
add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree
create_table "notification_settings", force: :cascade do |t|
@@ -836,11 +854,20 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.datetime "expires_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.string "scopes", default: "--- []\n", null: false
end
add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
+ create_table "project_authorizations", force: :cascade do |t|
+ t.integer "user_id"
+ t.integer "project_id"
+ t.integer "access_level"
+ end
+
+ add_index "project_authorizations", ["user_id", "project_id", "access_level"], name: "index_project_authorizations_on_user_id_project_id_access_level", unique: true, using: :btree
+
create_table "project_features", force: :cascade do |t|
t.integer "project_id"
t.integer "merge_requests_access_level"
@@ -908,11 +935,11 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.boolean "only_allow_merge_if_build_succeeds", default: false, null: false
t.boolean "has_external_issue_tracker"
t.string "repository_storage", default: "default", null: false
- t.boolean "request_access_enabled", default: true, null: false
+ t.boolean "request_access_enabled", default: false, null: false
t.boolean "has_external_wiki"
t.boolean "lfs_enabled"
t.text "description_html"
- t.boolean "only_allow_merge_if_all_discussions_are_resolved", default: false, null: false
+ t.boolean "only_allow_merge_if_all_discussions_are_resolved"
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -969,6 +996,17 @@ ActiveRecord::Schema.define(version: 20161106185620) do
add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree
add_index "releases", ["project_id"], name: "index_releases_on_project_id", using: :btree
+ create_table "routes", force: :cascade do |t|
+ t.integer "source_id", null: false
+ t.string "source_type", null: false
+ t.string "path", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "routes", ["path"], name: "index_routes_on_path", unique: true, using: :btree
+ add_index "routes", ["source_type", "source_id"], name: "index_routes_on_source_type_and_source_id", unique: true, using: :btree
+
create_table "sent_notifications", force: :cascade do |t|
t.integer "project_id"
t.integer "noteable_id"
@@ -1003,6 +1041,7 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.boolean "wiki_page_events", default: true
t.boolean "pipeline_events", default: false, null: false
t.boolean "confidential_issues_events", default: true, null: false
+ t.boolean "commit_events", default: true, null: false
end
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
@@ -1049,9 +1088,10 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.boolean "subscribed"
t.datetime "created_at"
t.datetime "updated_at"
+ t.integer "project_id"
end
- add_index "subscriptions", ["subscribable_id", "subscribable_type", "user_id"], name: "subscriptions_user_id_and_ref_fields", unique: true, using: :btree
+ add_index "subscriptions", ["subscribable_id", "subscribable_type", "user_id", "project_id"], name: "index_subscriptions_on_subscribable_and_user_id_and_project_id", unique: true, using: :btree
create_table "taggings", force: :cascade do |t|
t.integer "tag_id"
@@ -1184,6 +1224,7 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.boolean "external", default: false
t.string "organization"
t.string "incoming_email_token"
+ t.boolean "authorized_projects_populated"
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
@@ -1210,7 +1251,6 @@ ActiveRecord::Schema.define(version: 20161106185620) do
add_index "users_star_projects", ["project_id"], name: "index_users_star_projects_on_project_id", using: :btree
add_index "users_star_projects", ["user_id", "project_id"], name: "index_users_star_projects_on_user_id_and_project_id", unique: true, using: :btree
- add_index "users_star_projects", ["user_id"], name: "index_users_star_projects_on_user_id", using: :btree
create_table "web_hooks", force: :cascade do |t|
t.string "url", limit: 2000
@@ -1241,12 +1281,16 @@ ActiveRecord::Schema.define(version: 20161106185620) do
add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "lists", "boards"
add_foreign_key "lists", "labels"
+ add_foreign_key "merge_request_metrics", "ci_commits", column: "pipeline_id", on_delete: :cascade
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
add_foreign_key "personal_access_tokens", "users"
+ add_foreign_key "project_authorizations", "projects", on_delete: :cascade
+ add_foreign_key "project_authorizations", "users", on_delete: :cascade
add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
add_foreign_key "protected_branch_push_access_levels", "protected_branches"
+ add_foreign_key "subscriptions", "projects", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
end
diff --git a/doc/README.md b/doc/README.md
index c30bf328003..a60a5359540 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -1,4 +1,4 @@
-# Documentation
+# GitLab Community Edition documentation
## User documentation
@@ -8,7 +8,7 @@
- [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.
-- [Importing to GitLab](workflow/importing/README.md).
+- [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.
@@ -21,6 +21,7 @@
- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
- [University](university/README.md) Learn Git and GitLab through videos and courses.
- [Git Attributes](user/project/git_attributes.md) Managing Git attributes using a `.gitattributes` file.
+- [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf) Download a PDF describing the most used Git operations.
## Administrator documentation
diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md
index 07e548aaabe..2fc5d0355b5 100644
--- a/doc/administration/auth/README.md
+++ b/doc/administration/auth/README.md
@@ -7,5 +7,6 @@ providers.
and 389 Server
- [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google,
Bitbucket, Facebook, Shibboleth, Crowd and Azure
-- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS
+- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
+- [Okta](okta.md) Configure GitLab to sign in using Okta
diff --git a/doc/administration/auth/img/okta_admin_panel.png b/doc/administration/auth/img/okta_admin_panel.png
new file mode 100644
index 00000000000..12e21956715
--- /dev/null
+++ b/doc/administration/auth/img/okta_admin_panel.png
Binary files differ
diff --git a/doc/administration/auth/img/okta_saml_settings.png b/doc/administration/auth/img/okta_saml_settings.png
new file mode 100644
index 00000000000..ee275ece369
--- /dev/null
+++ b/doc/administration/auth/img/okta_saml_settings.png
Binary files differ
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index bf7814875bf..b8b63df091e 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -35,6 +35,10 @@ of one hour.
To enable LDAP integration you need to add your LDAP server settings in
`/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`.
+There is a Rake task to check LDAP configuration. After configuring LDAP
+using the documentation below, see [LDAP check Rake task](../raketasks/check.md#ldap-check)
+for information on the LDAP check Rake task.
+
>**Note**: In GitLab EE, you can configure multiple LDAP servers to connect to
one GitLab server.
@@ -217,7 +221,7 @@ Tip: If you want to limit access to the nested members of an Active Directory
group you can use the following syntax:
```
-(memberOf:1.2.840.113556.1.4.1941:=CN=My Group,DC=Example,DC=com)
+(memberOf=CN=My Group,DC=Example,DC=com)
```
Please note that GitLab does not support the custom filter syntax used by
@@ -253,6 +257,24 @@ the LDAP server's SSL certificate is performed.
## Troubleshooting
+### Debug LDAP user filter with ldapsearch
+
+This example uses ldapsearch and assumes you are using ActiveDirectory. The
+following query returns the login names of the users that will be allowed to
+log in to GitLab if you configure your own user_filter.
+
+```
+ldapsearch -H ldaps://$host:$port -D "$bind_dn" -y bind_dn_password.txt -b "$base" "$user_filter" sAMAccountName
+```
+
+- Variables beginning with a `$` refer to a variable from the LDAP section of
+ your configuration file.
+- Replace ldaps:// with ldap:// if you are using the plain authentication method.
+ Port `389` is the default `ldap://` port and `636` is the default `ldaps://`
+ port.
+- We are assuming the password for the bind_dn user is in bind_dn_password.txt.
+
+
### Invalid credentials when logging in
- Make sure the user you are binding with has enough permissions to read the user's
diff --git a/doc/administration/auth/okta.md b/doc/administration/auth/okta.md
new file mode 100644
index 00000000000..cb42b7743c5
--- /dev/null
+++ b/doc/administration/auth/okta.md
@@ -0,0 +1,160 @@
+# Okta SSO provider
+
+Okta is a [Single Sign-on provider][okta-sso] that can be used to authenticate
+with GitLab.
+
+The following documentation enables Okta as a SAML provider.
+
+## Configure the Okta application
+
+1. On Okta go to the admin section and choose to **Add an App**.
+1. When the app screen comes up you see another button to **Create an App** and
+ choose SAML 2.0 on the next screen.
+1. Now, very important, add a logo
+ (you can choose it from https://about.gitlab.com/press/). You'll have to
+ crop and resize it.
+1. Next, you'll need the to fill in the SAML general config. Here's an example
+ image.
+
+ ![Okta admin panel view](img/okta_admin_panel.png)
+
+1. The last part of the configuration is the feedback section where you can
+ just say you're a customer and creating an app for internal use.
+1. When you have your app you'll have a few tabs on the top of the app's
+ profile. Click on the SAML 2.0 config instructions button which should
+ look like the following:
+
+ ![Okta SAML settings](img/okta_saml_settings.png)
+
+1. On the screen that comes up take note of the
+ **Identity Provider Single Sign-On URL** which you'll use for the
+ `idp_sso_target_url` on your GitLab config file.
+
+1. **Before you leave Okta make sure you add your user and groups if any.**
+
+---
+
+Now that the Okta app is configured, it's time to enable it in GitLab.
+
+## Configure GitLab
+
+1. On your GitLab server, open the configuration file:
+
+ **For Omnibus GitLab installations**
+
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ **For installations from source**
+
+ ```sh
+ cd /home/git/gitlab
+ sudo -u git -H editor config/gitlab.yml
+ ```
+
+1. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration)
+ for initial settings.
+
+1. To allow your users to use Okta to sign up without having to manually create
+ an account first, don't forget to add the following values to your
+ configuration:
+
+ **For Omnibus GitLab installations**
+
+ ```ruby
+ gitlab_rails['omniauth_allow_single_sign_on'] = ['saml']
+ gitlab_rails['omniauth_block_auto_created_users'] = false
+ ```
+
+ **For installations from source**
+
+ ```yaml
+ allow_single_sign_on: ["saml"]
+ block_auto_created_users: false
+ ```
+
+1. You can also automatically link Okta users with existing GitLab users if
+ their email addresses match by adding the following setting:
+
+ **For Omnibus GitLab installations**
+
+ ```ruby
+ gitlab_rails['omniauth_auto_link_saml_user'] = true
+ ```
+
+ **For installations from source**
+
+ ```yaml
+ auto_link_saml_user: true
+ ```
+
+1. Add the provider configuration.
+
+ >**Notes:**
+ >- Change the value for `assertion_consumer_service_url` to match the HTTPS endpoint
+ of GitLab (append `users/auth/saml/callback` to the HTTPS URL of your GitLab
+ installation to generate the correct value).
+ >- To get the `idp_cert_fingerprint` fingerprint, first download the
+ certificate from the Okta app you registered and then run:
+ `openssl x509 -in okta.cert -noout -fingerprint`. Substitute `okta.cert`
+ with the location of your certificate.
+ >- Change the value of `idp_sso_target_url`, with the value of the
+ **Identity Provider Single Sign-On URL** from the step when you
+ configured the Okta app.
+ >- Change the value of `issuer` to a unique name, which will identify the application
+ to the IdP.
+ >- Leave `name_identifier_format` as-is.
+
+ **For Omnibus GitLab installations**
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ name: 'saml',
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://gitlab.oktapreview.com/app/gitlabdev773716_gitlabsaml_1/exk8odl81tBrjpD4B0h7/sso/saml',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ },
+ label: 'Okta' # optional label for SAML login button, defaults to "Saml"
+ }
+ ]
+ ```
+
+ **For installations from source**
+
+ ```yaml
+ - {
+ name: 'saml',
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://gitlab.oktapreview.com/app/gitlabdev773716_gitlabsaml_1/exk8odl81tBrjpD4B0h7/sso/saml',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ },
+ label: 'Okta' # optional label for SAML login button, defaults to "Saml"
+ }
+ ```
+
+
+1. [Reconfigure][reconf] or [restart] GitLab for Omnibus and installations
+ from source respectively for the changes to take effect.
+
+You might want to try this out on a incognito browser window.
+
+## Configuring groups
+
+>**Note:**
+Make sure the groups exist and are assigned to the Okta app.
+
+You can take a look of the [SAML documentation][saml] on external groups since
+it works the same.
+
+[okta-sso]: https://www.okta.com/products/single-sign-on/
+[saml]: ../../integration/saml.md#external-groups
+[reconf]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: ../restart_gitlab.md#installations-from-source
diff --git a/doc/administration/build_artifacts.md b/doc/administration/build_artifacts.md
index 64353f7282b..3ba8387c7f0 100644
--- a/doc/administration/build_artifacts.md
+++ b/doc/administration/build_artifacts.md
@@ -84,7 +84,7 @@ _The artifacts are stored by default in
## Set the maximum file size of the artifacts
Provided the artifacts are enabled, you can change the maximum file size of the
-artifacts through the [Admin area settings](../user/admin_area/settings/continuous_integration#maximum-artifacts-size).
+artifacts through the [Admin area settings](../user/admin_area/settings/continuous_integration.md#maximum-artifacts-size).
[reconfigure gitlab]: restart_gitlab.md "How to restart GitLab"
[restart gitlab]: restart_gitlab.md "How to restart GitLab"
diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md
index 0387d730489..80e5d80aa41 100644
--- a/doc/administration/custom_hooks.md
+++ b/doc/administration/custom_hooks.md
@@ -42,6 +42,33 @@ Follow the steps below to set up a custom hook:
That's it! Assuming the hook code is properly implemented the hook will fire
as appropriate.
+## Chained hooks support
+
+> [Introduced][93] in GitLab Shell 4.1.0 and GitLab 8.15.
+
+Hooks can be also placed in `hooks/<hook_name>.d` (global) or
+`custom_hooks/<hook_name>.d` (per project) directories supporting chained
+execution of the hooks.
+
+To look in a different directory for the global custom hooks (those in
+`hooks/<hook_name.d>`), set `custom_hooks_dir` in gitlab-shell config. For
+Omnibus installations, this can be set in `gitlab.rb`; and in source
+installations, this can be set in `gitlab-shell/config.yml`.
+
+The hooks are searched and executed in this order:
+
+1. `<project>.git/hooks/` - symlink to `gitlab-shell/hooks` global dir
+1. `<project>.git/hooks/<hook_name>` - executed by `git` itself, this is `gitlab-shell/hooks/<hook_name>`
+1. `<project>.git/custom_hooks/<hook_name>` - per project hook (this is already existing behavior)
+1. `<project>.git/custom_hooks/<hook_name>.d/*` - per project hooks
+1. `<project>.git/hooks/<hook_name>.d/*` OR `<custom_hooks_dir>/<hook_name.d>/*` - global hooks: all executable files (minus editor backup files)
+
+Files in `.d` directories need to be executable and not match the backup file
+pattern (`*~`).
+
+The hooks of the same type are executed in order and execution stops on the
+first script exiting with a non-zero value.
+
## Custom error messages
> [Introduced][5073] in GitLab 8.10.
@@ -54,3 +81,4 @@ STDERR takes precedence over STDOUT.
[hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks
[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/environment_variables.md b/doc/administration/environment_variables.md
index b4a953d1ccc..76029b30dd8 100644
--- a/doc/administration/environment_variables.md
+++ b/doc/administration/environment_variables.md
@@ -13,17 +13,18 @@ override certain values.
Variable | Type | Description
-------- | ---- | -----------
-`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation
-`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`)
-`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test`
-`DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development`
-`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab
-`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab
-`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
-`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
-`GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab
-`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer
-`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer
+`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation
+`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`)
+`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test`
+`DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development`
+`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab
+`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab
+`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
+`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
+`GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab
+`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer
+`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer
+`GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN` | string | Sets the initial registration token used for GitLab Runners
## Complete database variables
diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md
index d74a786ac24..d5a5aef7ec0 100644
--- a/doc/administration/high_availability/README.md
+++ b/doc/administration/high_availability/README.md
@@ -7,19 +7,10 @@ highly available.
## Architecture
-### Active/Passive
-
-For pure high-availability/failover with no scaling you can use an
-active/passive configuration. This utilizes DRBD (Distributed Replicated
-Block Device) to keep all data in sync. DRBD requires a low latency link to
-remain in sync. It is not advisable to attempt to run DRBD between data centers
-or in different cloud availability zones.
+There are two kinds of setups:
-Components/Servers Required:
-
-- 2 servers/virtual machines (one active/one passive)
-
-![Active/Passive HA Diagram](../img/high_availability/active-passive-diagram.png)
+- active/active
+- active/passive
### Active/Active
@@ -28,12 +19,24 @@ user requests simultaneously. The database, Redis, and GitLab application are
all deployed on separate servers. The configuration is **only** highly-available
if the database, Redis and storage are also configured as such.
-![Active/Active HA Diagram](../img/high_availability/active-active-diagram.png)
-
-**Steps to configure active/active:**
+Follow the steps below to configure an active/active setup:
1. [Configure the database](database.md)
1. [Configure Redis](redis.md)
1. [Configure NFS](nfs.md)
1. [Configure the GitLab application servers](gitlab.md)
1. [Configure the load balancers](load_balancer.md)
+
+![Active/Active HA Diagram](../img/high_availability/active-active-diagram.png)
+
+### Active/Passive
+
+For pure high-availability/failover with no scaling you can use an
+active/passive configuration. This utilizes DRBD (Distributed Replicated
+Block Device) to keep all data in sync. DRBD requires a low latency link to
+remain in sync. It is not advisable to attempt to run DRBD between data centers
+or in different cloud availability zones.
+
+Components/Servers Required: 2 servers/virtual machines (one active/one passive)
+
+![Active/Passive HA Diagram](../img/high_availability/active-passive-diagram.png)
diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md
index 538dada1bae..e4f94eb7cb6 100644
--- a/doc/administration/high_availability/database.md
+++ b/doc/administration/high_availability/database.md
@@ -41,9 +41,12 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
mailroom['enable'] = false
# PostgreSQL configuration
- postgresql['sql_password'] = 'DB password'
+ gitlab_rails['db_password'] = 'DB password'
postgresql['md5_auth_cidr_addresses'] = ['0.0.0.0/0']
postgresql['listen_address'] = '0.0.0.0'
+
+ # Disable automatic database migrations
+ gitlab_rails['auto_migrate'] = false
```
1. Run `sudo gitlab-ctl reconfigure` to install and configure PostgreSQL.
@@ -80,7 +83,7 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
1. Similarly, set the password for the `gitlab` database user. Use the same
password that you specified in the `/etc/gitlab/gitlab.rb` file for
- `postgresql['sql_password']`.
+ `gitlab_rails['db_password']`.
```
\password gitlab
@@ -102,9 +105,6 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
1. Exit the database prompt by typing `\q` and Enter.
1. Exit the `gitlab-psql` user by running `exit` twice.
1. Run `sudo gitlab-ctl reconfigure` a final time.
-1. Run `touch /etc/gitlab/skip-auto-migrations` to prevent database migrations
- from running on upgrade. Only the primary GitLab application server should
- handle migrations.
---
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index 537f4f3501d..5602d70f1ef 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -76,7 +76,7 @@ configuration to move each data location to a subdirectory:
user['home'] = '/gitlab-data/home'
git_data_dir '/gitlab-data/git-data'
gitlab_rails['shared_path'] = '/gitlab-data/shared'
-gitlab_rails['uploads_directory'] = "/gitlab-data/uploads"
+gitlab_rails['uploads_directory'] = '/gitlab-data/uploads'
gitlab_ci['builds_directory'] = '/gitlab-data/builds'
```
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index bc424330656..b4e7bf21e35 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -1,265 +1,780 @@
# Configuring Redis for GitLab HA
-You can choose to install and manage Redis yourself, or you can use the one
-that comes bundled with GitLab Omnibus packages.
-
-> **Note:** Redis does not require authentication by default. See
+>
+Experimental Redis Sentinel support was [Introduced][ce-1877] in GitLab 8.11.
+Starting with 8.14, Redis Sentinel is no longer experimental.
+If you've used it with versions `< 8.14` before, please check the updated
+documentation here.
+
+High Availability with [Redis] is possible using a **Master** x **Slave**
+topology with a [Redis Sentinel][sentinel] service to watch and automatically
+start the failover procedure.
+
+You can choose to install and manage Redis and Sentinel yourself, use
+a hosted cloud solution or you can use the one that comes bundled with
+Omnibus GitLab packages.
+
+> **Notes:**
+- Redis requires authentication for High Availability. See
[Redis Security](http://redis.io/topics/security) documentation for more
information. We recommend using a combination of a Redis password and tight
firewall rules to secure your Redis service.
+- You are highly encouraged to read the [Redis Sentinel][sentinel] documentation
+ before configuring Redis HA with GitLab to fully understand the topology and
+ architecture.
+- This is the documentation for the Omnibus GitLab packages. For installations
+ from source, follow the [Redis HA source installation](redis_source.md) guide.
+- Redis Sentinel daemon is bundled with Omnibus GitLab Enterprise Edition only.
+ For configuring Sentinel with the Omnibus GitLab Community Edition and
+ installations from source, read the
+ [Available configuration setups](#available-configuration-setups) section
+ below.
+
+## Overview
+
+Before diving into the details of setting up Redis and Redis Sentinel for HA,
+make sure you read this Overview section to better understand how the components
+are tied together.
+
+You need at least `3` independent machines: physical, or VMs running into
+distinct physical machines. It is essential that all master and slaves Redis
+instances run in different machines. If you fail to provision the machines in
+that specific way, any issue with the shared environment can bring your entire
+setup down.
+
+It is OK to run a Sentinel along with a master or slave Redis instance.
+No more than one Sentinel in the same machine though.
+
+You also need to take in consideration the underlying network topology,
+making sure you have redundant connectivity between Redis / Sentinel and
+GitLab instances, otherwise the networks will become a single point of
+failure.
+
+Make sure that you read this document once as a whole before configuring the
+components below.
+
+### High Availability with Sentinel
+
+>**Notes:**
+- Starting with GitLab `8.11`, you can configure a list of Redis Sentinel
+ servers that will monitor a group of Redis servers to provide failover support.
+- Starting with GitLab `8.14`, the Omnibus GitLab Enterprise Edition package
+ comes with Redis Sentinel daemon built-in.
+
+High Availability with Redis requires a few things:
+
+- Multiple Redis instances
+- Run Redis in a **Master** x **Slave** topology
+- Multiple Sentinel instances
+- Application support and visibility to all Sentinel and Redis instances
+
+Redis Sentinel can handle the most important tasks in an HA environment and that's
+to help keep servers online with minimal to no downtime. Redis Sentinel:
+
+- Monitors **Master** and **Slaves** instances to see if they are available
+- Promotes a **Slave** to **Master** when the **Master** fails
+- Demotes a **Master** to **Slave** when the failed **Master** comes back online
+ (to prevent data-partitioning)
+- Can be queried by the application to always connect to the current **Master**
+ server
+
+When a **Master** fails to respond, it's the application's responsibility
+(in our case GitLab) to handle timeout and reconnect (querying a **Sentinel**
+for a new **Master**).
-## Configure your own Redis server
+To get a better understanding on how to correctly setup Sentinel, please read
+the [Redis Sentinel documentation](http://redis.io/topics/sentinel) first, as
+failing to configure it correctly can lead to data loss or can bring your
+whole cluster down, invalidating the failover effort.
-If you're hosting GitLab on a cloud provider, you can optionally use a
-managed service for Redis. For example, AWS offers a managed ElastiCache service
-that runs Redis.
+### Recommended setup
-## Configure Redis using Omnibus
+For a minimal setup, you will install the Omnibus GitLab package in `3`
+**independent** machines, both with **Redis** and **Sentinel**:
-If you don't want to bother setting up your own Redis server, you can use the
-one bundled with Omnibus. In this case, you should disable all services except
-Redis.
+- Redis Master + Sentinel
+- Redis Slave + Sentinel
+- Redis Slave + Sentinel
-1. Download/install GitLab Omnibus using **steps 1 and 2** from
- [GitLab downloads](https://about.gitlab.com/downloads). Do not complete other
- steps on the download page.
-1. Create/edit `/etc/gitlab/gitlab.rb` and use the following configuration.
- Be sure to change the `external_url` to match your eventual GitLab front-end
- URL:
+If you are not sure or don't understand why and where the amount of nodes come
+from, read [Redis setup overview](#redis-setup-overview) and
+[Sentinel setup overview](#sentinel-setup-overview).
- ```ruby
- external_url 'https://gitlab.example.com'
-
- # Disable all services except Redis
- redis['enable'] = true
- bootstrap['enable'] = false
- nginx['enable'] = false
- unicorn['enable'] = false
- sidekiq['enable'] = false
- postgresql['enable'] = false
- gitlab_workhorse['enable'] = false
- mailroom['enable'] = false
-
- # Redis configuration
- redis['port'] = 6379
- redis['bind'] = '0.0.0.0'
+For a recommended setup that can resist more failures, you will install
+the Omnibus GitLab package in `5` **independent** machines, both with
+**Redis** and **Sentinel**:
- # If you wish to use Redis authentication (recommended)
- redis['password'] = 'Redis Password'
- ```
+- Redis Master + Sentinel
+- Redis Slave + Sentinel
+- Redis Slave + Sentinel
+- Redis Slave + Sentinel
+- Redis Slave + Sentinel
-1. Run `sudo gitlab-ctl reconfigure` to install and configure PostgreSQL.
+### Redis setup overview
- > **Note**: This `reconfigure` step will result in some errors.
- That's OK - don't be alarmed.
+You must have at least `3` Redis servers: `1` Master, `2` Slaves, and they
+need to be each in a independent machine (see explanation above).
-1. Run `touch /etc/gitlab/skip-auto-migrations` to prevent database migrations
- from running on upgrade. Only the primary GitLab application server should
- handle migrations.
+You can have additional Redis nodes, that will help survive a situation
+where more nodes goes down. Whenever there is only `2` nodes online, a failover
+will not be initiated.
-## Experimental Redis Sentinel support
+As an example, if you have `6` Redis nodes, a maximum of `3` can be
+simultaneously down.
-> [Introduced][ce-1877] in GitLab 8.11.
+Please note that there are different requirements for Sentinel nodes.
+If you host them in the same Redis machines, you may need to take
+that restrictions into consideration when calculating the amount of
+nodes to be provisioned. See [Sentinel setup overview](#sentinel-setup-overview)
+documentation for more information.
-Since GitLab 8.11, you can configure a list of Redis Sentinel servers that
-will monitor a group of Redis servers to provide you with a standard failover
-support.
+All Redis nodes should be configured the same way and with similar server specs, as
+in a failover situation, any **Slave** can be promoted as the new **Master** by
+the Sentinel servers.
-There is currently one exception to the Sentinel support: `mail_room`, the
-component that processes incoming emails. It doesn't support Sentinel yet, but
-we hope to integrate a future release that does support it.
+The replication requires authentication, so you need to define a password to
+protect all Redis nodes and the Sentinels. They will all share the same
+password, and all instances must be able to talk to
+each other over the network.
-To get a better understanding on how to correctly setup Sentinel, please read
-the [Redis Sentinel documentation](http://redis.io/topics/sentinel) first, as
-failing to configure it correctly can lead to data loss.
+### Sentinel setup overview
-The configuration consists of three parts:
+Sentinels watch both other Sentinels and Redis nodes. Whenever a Sentinel
+detects that a Redis node is not responding, it will announce that to the
+other Sentinels. They have to reach the **quorum**, that is the minimum amount
+of Sentinels that agrees a node is down, in order to be able to start a failover.
-- Redis setup
-- Sentinel setup
-- GitLab setup
+Whenever the **quorum** is met, the **majority** of all known Sentinel nodes
+need to be available and reachable, so that they can elect the Sentinel **leader**
+who will take all the decisions to restore the service availability by:
-Read carefully how to configure those components below.
+- Promoting a new **Master**
+- Reconfiguring the other **Slaves** and make them point to the new **Master**
+- Announce the new **Master** to every other Sentinel peer
+- Reconfigure the old **Master** and demote to **Slave** when it comes back online
-### Redis setup
+You must have at least `3` Redis Sentinel servers, and they need to
+be each in a independent machine (that are believed to fail independently),
+ideally in different geographical areas.
-You must have at least 2 Redis servers: 1 Master, 1 or more Slaves.
-They should be configured the same way and with similar server specs, as
-in a failover situation, any Slave can be elected as the new Master by
-the Sentinel servers.
+You can configure them in the same machines where you've configured the other
+Redis servers, but understand that if a whole node goes down, you loose both
+a Sentinel and a Redis instance.
-In a minimal setup, the only required change for the slaves in `redis.conf`
-is the addition of a `slaveof` line pointing to the initial master.
-You can increase the security by defining a `requirepass` configuration in
-the master, and `masterauth` in slaves.
+The number of sentinels should ideally always be an **odd** number, for the
+consensus algorithm to be effective in the case of a failure.
----
+In a `3` nodes topology, you can only afford `1` Sentinel node going down.
+Whenever the **majority** of the Sentinels goes down, the network partition
+protection prevents destructive actions and a failover **will not be started**.
-**Configuring your own Redis server**
+Here are some examples:
-1. Add to the slaves' `redis.conf`:
+- With `5` or `6` sentinels, a maximum of `2` can go down for a failover begin.
+- With `7` sentinels, a maximum of `3` nodes can go down.
- ```conf
- # IP and port of the master Redis server
- slaveof 10.10.10.10 6379
- ```
+The **Leader** election can sometimes fail the voting round when **consensus**
+is not achieved (see the odd number of nodes requirement above). In that case,
+a new attempt will be made after the amount of time defined in
+`sentinel['failover_timeout']` (in milliseconds).
-1. Optionally, set up password authentication for increased security.
- Add the following to master's `redis.conf`:
+>**Note:**
+We will see where `sentinel['failover_timeout']` is defined later.
+
+The `failover_timeout` variable has a lot of different use cases. According to
+the official documentation:
+
+- The time needed to re-start a failover after a previous failover was
+ already tried against the same master by a given Sentinel, is two
+ times the failover timeout.
+
+- The time needed for a slave replicating to a wrong master according
+ to a Sentinel current configuration, to be forced to replicate
+ with the right master, is exactly the failover timeout (counting since
+ the moment a Sentinel detected the misconfiguration).
+
+- The time needed to cancel a failover that is already in progress but
+ did not produced any configuration change (SLAVEOF NO ONE yet not
+ acknowledged by the promoted slave).
+
+- The maximum time a failover in progress waits for all the slaves to be
+ reconfigured as slaves of the new master. However even after this time
+ the slaves will be reconfigured by the Sentinels anyway, but not with
+ the exact parallel-syncs progression as specified.
+
+### Available configuration setups
+
+Based on your infrastructure setup and how you have installed GitLab, there are
+multiple ways to configure Redis HA. Omnibus GitLab packages have Redis and/or
+Redis Sentinel bundled with them so you only need to focus on configuration.
+Pick the one that suits your needs.
+
+- [Installations from source][source]: You need to install Redis and Sentinel
+ yourself. Use the [Redis HA installation from source](redis_source.md)
+ documentation.
+- [Omnibus GitLab **Community Edition** (CE) package][ce]: Redis is bundled, so you
+ can use the package with only the Redis service enabled as described in steps
+ 1 and 2 of this document (works for both master and slave setups). To install
+ and configure Sentinel, jump directly to the Sentinel section in the
+ [Redis HA installation from source](redis_source.md#step-3-configuring-the-redis-sentinel-instances) documentation.
+- [Omnibus GitLab **Enterprise Edition** (EE) package][ee]: Both Redis and Sentinel
+ are bundled in the package, so you can use the EE package to setup the whole
+ Redis HA infrastructure (master, slave and Sentinel) which is described in
+ this document.
+- If you have installed GitLab using the Omnibus GitLab packages (CE or EE),
+ but you want to use your own external Redis server, follow steps 1-3 in the
+ [Redis HA installation from source](redis_source.md) documentation, then go
+ straight to step 4 in this guide to
+ [set up the GitLab application](#step-4-configuring-the-gitlab-application).
+
+## Configuring Redis HA
+
+This is the section where we install and setup the new Redis instances.
+
+>**Notes:**
+- We assume that you install GitLab and all HA components from scratch. If you
+ already have it installed and running, read how to
+ [switch from a single-machine installation to Redis HA](#switching-from-an-existing-single-machine-installation-to-redis-ha).
+- Redis nodes (both master and slaves) will need the same password defined in
+ `redis['password']`. At any time during a failover the Sentinels can
+ reconfigure a node and change its status from master to slave and vice versa.
+
+### Prerequisites
+
+The prerequisites for a HA Redis setup are the following:
+
+1. Provision the minimum required number of instances as specified in the
+ [recommended setup](#recommended-setup) section.
+1. **Do NOT** install Redis or Redis Sentinel in the same machines your
+ GitLab application is running on. You can however opt in to install Redis
+ and Sentinel in the same machine (each in independent ones is recommended
+ though).
+1. All Redis nodes must be able to talk to each other and accept incoming
+ connections over Redis (`6379`) and Sentinel (`26379`) ports (unless you
+ change the default ones).
+1. The server that hosts the GitLab application must be able to access the
+ Redis nodes.
+1. Protect the nodes from access from external networks ([Internet][it]), using
+ firewall.
+
+### Step 1. Configuring the master Redis instance
+
+1. SSH into the **master** Redis server.
+1. [Download/install](https://about.gitlab.com/installation) the Omnibus GitLab
+ package you want using **steps 1 and 2** from the GitLab downloads page.
+ - Make sure you select the correct Omnibus package, with the same version
+ and type (Community, Enterprise editions) of your current install.
+ - Do not complete any other steps on the download page.
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the contents:
- ```conf
- # Optional password authentication for increased security
- requirepass "<password>"
+ ```ruby
+ # Enable the master role and disable all other services in the machine
+ # (you can still enable Sentinel).
+ redis_master_role['enable'] = true
+
+ # IP address pointing to a local IP that the other machines can reach to.
+ # You can also set bind to '0.0.0.0' which listen in all interfaces.
+ # If you really need to bind to an external accessible IP, make
+ # sure you add extra firewall rules to prevent unauthorized access.
+ redis['bind'] = '10.0.0.1'
+
+ # Define a port so Redis can listen for TCP requests which will allow other
+ # machines to connect to it.
+ redis['port'] = 6379
+
+ # Set up password authentication for Redis (use the same password in all nodes).
+ redis['password'] = 'redis-password-goes-here'
```
-1. Then add this line to all the slave servers' `redis.conf`:
+1. Only the primary GitLab application server should handle migrations. To
+ prevent database migrations from running on upgrade, add the following
+ configuration to your `/etc/gitlab/gitlab.rb` file:
- ```conf
- masterauth "<password>"
+ ```
+ gitlab_rails['auto_migrate'] = false
```
-1. Restart the Redis services for the changes to take effect.
+1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
----
+### Step 2. Configuring the slave Redis instances
-**Using Redis via Omnibus**
+1. SSH into the **slave** Redis server.
+1. [Download/install](https://about.gitlab.com/installation) the Omnibus GitLab
+ package you want using **steps 1 and 2** from the GitLab downloads page.
+ - Make sure you select the correct Omnibus package, with the same version
+ and type (Community, Enterprise editions) of your current install.
+ - Do not complete any other steps on the download page.
-1. Edit `/etc/gitlab/gitlab.rb` of a master Redis machine (usualy a single machine):
+1. Edit `/etc/gitlab/gitlab.rb` and add the contents:
```ruby
- ## Redis TCP support (will disable UNIX socket transport)
- redis['bind'] = '0.0.0.0' # or specify an IP to bind to a single one
+ # Enable the slave role and disable all other services in the machine
+ # (you can still enable Sentinel). This will also set automatically
+ # `redis['master'] = false`.
+ redis_slave_role['enable'] = true
+
+ # IP address pointing to a local IP that the other machines can reach to.
+ # You can also set bind to '0.0.0.0' which listen in all interfaces.
+ # If you really need to bind to an external accessible IP, make
+ # sure you add extra firewall rules to prevent unauthorized access.
+ redis['bind'] = '10.0.0.2'
+
+ # Define a port so Redis can listen for TCP requests which will allow other
+ # machines to connect to it.
redis['port'] = 6379
- ## Master redis instance
- redis['password'] = '<huge password string here>'
- ```
+ # The same password for Redeis authentication you set up for the master node.
+ redis['password'] = 'redis-password-goes-here'
-1. Edit `/etc/gitlab/gitlab.rb` of a slave Redis machine (should be one or more machines):
+ # The IP of the master Redis node.
+ redis['master_ip'] = '10.0.0.1'
- ```ruby
- ## Redis TCP support (will disable UNIX socket transport)
- redis['bind'] = '0.0.0.0' # or specify an IP to bind to a single one
- redis['port'] = 6379
+ # Port of master Redis server, uncomment to change to non default. Defaults
+ # to `6379`.
+ #redis['master_port'] = 6379
+ ```
- ## Slave redis instance
- redis['master_ip'] = '10.10.10.10' # IP of master Redis server
- redis['master_port'] = 6379 # Port of master Redis server
- redis['master_password'] = "<huge password string here>"
+1. To prevent database migrations from running on upgrade, run:
+
+ ```
+ sudo touch /etc/gitlab/skip-auto-migrations
```
-1. Reconfigure the GitLab for the changes to take effect: `sudo gitlab-ctl reconfigure`
+ Only the primary GitLab application server should handle migrations.
+
+1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+1. Go through the steps again for all the other slave nodes.
---
+These values don't have to be changed again in `/etc/gitlab/gitlab.rb` after
+a failover, as the nodes will be managed by the Sentinels, and even after a
+`gitlab-ctl reconfigure`, they will get their configuration restored by
+the same Sentinels.
+
+### Step 3. Configuring the Redis Sentinel instances
+
+>**Note:**
+Redis Sentinel is bundled with Omnibus GitLab Enterprise Edition only. The
+following section assumes you are using Omnibus GitLab Enterprise Edition.
+For the Omnibus Community Edition and installations from source, follow the
+[Redis HA source install](redis_source.md) guide.
+
Now that the Redis servers are all set up, let's configure the Sentinel
servers.
-### Sentinel setup
+If you are not sure if your Redis servers are working and replicating
+correctly, please read the [Troubleshooting Replication](#troubleshooting-replication)
+and fix it before proceeding with Sentinel setup.
-We don't provide yet an automated way to setup and run the Sentinel daemon
-from Omnibus installation method. You must follow the instructions below and
-run it by yourself.
+You must have at least `3` Redis Sentinel servers, and they need to
+be each in an independent machine. You can configure them in the same
+machines where you've configured the other Redis servers.
-The support for Sentinel in Ruby has some [caveats](https://github.com/redis/redis-rb/issues/531).
-While you can give any name for the `master-group-name` part of the
-configuration, as in this example:
+With GitLab Enterprise Edition, you can use the Omnibus package to setup
+multiple machines with the Sentinel daemon.
-```conf
-sentinel monitor <master-group-name> <ip> <port> <quorum>
-```
+---
-,for it to work in Ruby, you have to use the "hostname" of the master Redis
-server, otherwise you will get an error message like:
-`Redis::CannotConnectError: No sentinels available.`. Read
-[Sentinel troubleshooting](#sentinel-troubleshooting) for more information.
+1. SSH into the server that will host Redis Sentinel.
+1. **You can omit this step if the Sentinels will be hosted in the same node as
+ the other Redis instances.**
-Here is an example configuration file (`sentinel.conf`) for a Sentinel node:
+ [Download/install](https://about.gitlab.com/downloads-ee) the
+ Omnibus GitLab Enterprise Edition package using **steps 1 and 2** from the
+ GitLab downloads page.
+ - Make sure you select the correct Omnibus package, with the same version
+ the GitLab application is running.
+ - Do not complete any other steps on the download page.
-```conf
-port 26379
-sentinel monitor master-redis.example.com 10.10.10.10 6379 1
-sentinel down-after-milliseconds master-redis.example.com 10000
-sentinel config-epoch master-redis.example.com 0
-sentinel leader-epoch master-redis.example.com 0
-```
+1. Edit `/etc/gitlab/gitlab.rb` and add the contents (if you are installing the
+ Sentinels in the same node as the other Redis instances, some values might
+ be duplicate below):
----
+ ```ruby
+ redis_sentinel_role['enable'] = true
-The final part is to inform the main GitLab application server of the Redis
-master and the new sentinels servers.
+ # Must be the same in every sentinel node
+ redis['master_name'] = 'gitlab-redis'
-### GitLab setup
+ # The same password for Redis authentication you set up for the master node.
+ redis['password'] = 'redis-password-goes-here'
-You can enable or disable sentinel support at any time in new or existing
-installations. From the GitLab application perspective, all it requires is
-the correct credentials for the master Redis and for a few Sentinel nodes.
+ # The IP of the master Redis node.
+ redis['master_ip'] = '10.0.0.1'
-It doesn't require a list of all Sentinel nodes, as in case of a failure,
-the application will need to query only one of them.
+ # Define a port so Redis can listen for TCP requests which will allow other
+ # machines to connect to it.
+ redis['port'] = 6379
->**Note:**
-The following steps should be performed in the [GitLab application server](gitlab.md).
+ # Port of master Redis server, uncomment to change to non default. Defaults
+ # to `6379`.
+ #redis['master_port'] = 6379
+
+ ## Configure Sentinel
+ sentinel['bind'] = '10.0.0.1'
+
+ # Port that Sentinel listens on, uncomment to change to non default. Defaults
+ # to `26379`.
+ # sentinel['port'] = 26379
+
+ ## Quorum must reflect the amount of voting sentinels it take to start a failover.
+ ## Value must NOT be greater then the amount of sentinels.
+ ##
+ ## The quorum can be used to tune Sentinel in two ways:
+ ## 1. If a the quorum is set to a value smaller than the majority of Sentinels
+ ## we deploy, we are basically making Sentinel more sensible to master failures,
+ ## triggering a failover as soon as even just a minority of Sentinels is no longer
+ ## able to talk with the master.
+ ## 1. If a quorum is set to a value greater than the majority of Sentinels, we are
+ ## making Sentinel able to failover only when there are a very large number (larger
+ ## than majority) of well connected Sentinels which agree about the master being down.s
+ sentinel['quorum'] = 2
+
+ ## Consider unresponsive server down after x amount of ms.
+ # sentinel['down_after_milliseconds'] = 10000
+
+ ## Specifies the failover timeout in milliseconds. It is used in many ways:
+ ##
+ ## - The time needed to re-start a failover after a previous failover was
+ ## already tried against the same master by a given Sentinel, is two
+ ## times the failover timeout.
+ ##
+ ## - The time needed for a slave replicating to a wrong master according
+ ## to a Sentinel current configuration, to be forced to replicate
+ ## with the right master, is exactly the failover timeout (counting since
+ ## the moment a Sentinel detected the misconfiguration).
+ ##
+ ## - The time needed to cancel a failover that is already in progress but
+ ## did not produced any configuration change (SLAVEOF NO ONE yet not
+ ## acknowledged by the promoted slave).
+ ##
+ ## - The maximum time a failover in progress waits for all the slaves to be
+ ## reconfigured as slaves of the new master. However even after this time
+ ## the slaves will be reconfigured by the Sentinels anyway, but not with
+ ## the exact parallel-syncs progression as specified.
+ # sentinel['failover_timeout'] = 60000
+ ```
-**For source based installations**
+1. To prevent database migrations from running on upgrade, run:
-1. Edit `/home/git/gitlab/config/resque.yml` following the example in
- `/home/git/gitlab/config/resque.yml.example`, and uncomment the sentinels
- line, changing to the correct server credentials.
-1. Restart GitLab for the changes to take effect.
+ ```
+ sudo touch /etc/gitlab/skip-auto-migrations
+ ```
-**For Omnibus installations**
+ Only the primary GitLab application server should handle migrations.
+1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+1. Go through the steps again for all the other Sentinel nodes.
+
+### Step 4. Configuring the GitLab application
+
+The final part is to inform the main GitLab application server of the Redis
+Sentinels servers and authentication credentials.
+
+You can enable or disable Sentinel support at any time in new or existing
+installations. From the GitLab application perspective, all it requires is
+the correct credentials for the Sentinel nodes.
+
+While it doesn't require a list of all Sentinel nodes, in case of a failure,
+it needs to access at least one of the listed.
+
+>**Note:**
+The following steps should be performed in the [GitLab application server](gitlab.md)
+which ideally should not have Redis or Sentinels on it for a HA setup.
+
+1. SSH into the server where the GitLab application is installed.
1. Edit `/etc/gitlab/gitlab.rb` and add/change the following lines:
- ```ruby
- gitlab-rails['redis_host'] = "master-redis.example.com"
- gitlab-rails['redis_port'] = 6379
- gitlab-rails['redis_password'] = '<huge password string here>'
- gitlab-rails['redis_sentinels'] = [
- {'host' => '10.10.10.1', 'port' => 26379},
- {'host' => '10.10.10.2', 'port' => 26379},
- {'host' => '10.10.10.3', 'port' => 26379}
+ ```
+ ## Must be the same in every sentinel node
+ redis['master_name'] = 'gitlab-redis'
+
+ ## The same password for Redis authentication you set up for the master node.
+ redis['password'] = 'redis-password-goes-here'
+
+ ## A list of sentinels with `host` and `port`
+ gitlab_rails['redis_sentinels'] = [
+ {'host' => '10.0.0.1', 'port' => 26379},
+ {'host' => '10.0.0.2', 'port' => 26379},
+ {'host' => '10.0.0.3', 'port' => 26379}
]
```
-1. [Reconfigure] the GitLab for the changes to take effect.
+1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
-### Sentinel troubleshooting
+## Switching from an existing single-machine installation to Redis HA
-If you get an error like: `Redis::CannotConnectError: No sentinels available.`,
-there may be something wrong with your configuration files or it can be related
-to [this issue][gh-531] ([pull request][gh-534] that should make things better).
+If you already have a single-machine GitLab install running, you will need to
+replicate from this machine first, before de-activating the Redis instance
+inside it.
+
+Your single-machine install will be the initial **Master**, and the `3` others
+should be configured as **Slave** pointing to this machine.
+
+After replication catches up, you will need to stop services in the
+single-machine install, to rotate the **Master** to one of the new nodes.
-It's a bit rigid the way you have to config `resque.yml` and `sentinel.conf`,
-otherwise `redis-rb` will not work properly.
+Make the required changes in configuration and restart the new nodes again.
-The hostname ('my-primary-redis') of the primary Redis server (`sentinel.conf`)
-**must** match the one configured in GitLab (`resque.yml` for source installations
-or `gitlab-rails['redis_*']` in Omnibus) and it must be valid ex:
+To disable redis in the single install, edit `/etc/gitlab/gitlab.rb`:
-```conf
-# sentinel.conf:
-sentinel monitor my-primary-redis 10.10.10.10 6379 1
-sentinel down-after-milliseconds my-primary-redis 10000
-sentinel config-epoch my-primary-redis 0
-sentinel leader-epoch my-primary-redis 0
+```ruby
+redis['enable'] = false
```
-```yaml
-# resque.yaml
-production:
- url: redis://my-primary-redis:6378
- sentinels:
- -
- host: slave1
- port: 26380 # point to sentinel, not to redis port
- -
- host: slave2
- port: 26381 # point to sentinel, not to redis port
+If you fail to replicate first, you may loose data (unprocessed background jobs).
+
+## Example of a minimal configuration with 1 master, 2 slaves and 3 Sentinels
+
+>**Note:**
+Redis Sentinel is bundled with Omnibus GitLab Enterprise Edition only. For
+different setups, read the
+[available configuration setups](#available-configuration-setups) section.
+
+In this example we consider that all servers have an internal network
+interface with IPs in the `10.0.0.x` range, and that they can connect
+to each other using these IPs.
+
+In a real world usage, you would also setup firewall rules to prevent
+unauthorized access from other machines and block traffic from the
+outside (Internet).
+
+We will use the same `3` nodes with **Redis** + **Sentinel** topology
+discussed in [Redis setup overview](#redis-setup-overview) and
+[Sentinel setup overview](#sentinel-setup-overview) documentation.
+
+Here is a list and description of each **machine** and the assigned **IP**:
+
+* `10.0.0.1`: Redis Master + Sentinel 1
+* `10.0.0.2`: Redis Slave 1 + Sentinel 2
+* `10.0.0.3`: Redis Slave 2 + Sentinel 3
+* `10.0.0.4`: GitLab application
+
+Please note that after the initial configuration, if a failover is initiated
+by the Sentinel nodes, the Redis nodes will be reconfigured and the **Master**
+will change permanently (including in `redis.conf`) from one node to the other,
+until a new failover is initiated again.
+
+The same thing will happen with `sentinel.conf` that will be overridden after the
+initial execution, after any new sentinel node starts watching the **Master**,
+or a failover promotes a different **Master** node.
+
+### Example configuration for Redis master and Sentinel 1
+
+In `/etc/gitlab/gitlab.rb`:
+
+```ruby
+redis_master_role['enable'] = true
+redis_sentinel_role['enable'] = true
+redis['bind'] = '10.0.0.1'
+redis['port'] = 6379
+redis['password'] = 'redis-password-goes-here'
+redis['master_name'] = 'gitlab-redis' # must be the same in every sentinel node
+redis['master_password'] = 'redis-password-goes-here' # the same value defined in redis['password'] in the master instance
+redis['master_ip'] = '10.0.0.1' # ip of the initial master redis instance
+#redis['master_port'] = 6379 # port of the initial master redis instance, uncomment to change to non default
+sentinel['bind'] = '10.0.0.1'
+# sentinel['port'] = 26379 # uncomment to change default port
+sentinel['quorum'] = 2
+# sentinel['down_after_milliseconds'] = 10000
+# sentinel['failover_timeout'] = 60000
+```
+
+[Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+
+### Example configuration for Redis slave 1 and Sentinel 2
+
+In `/etc/gitlab/gitlab.rb`:
+
+```ruby
+redis_slave_role['enable'] = true
+redis_sentinel_role['enable'] = true
+redis['bind'] = '10.0.0.2'
+redis['port'] = 6379
+redis['password'] = 'redis-password-goes-here'
+redis['master_password'] = 'redis-password-goes-here'
+redis['master_ip'] = '10.0.0.1' # IP of master Redis server
+#redis['master_port'] = 6379 # Port of master Redis server, uncomment to change to non default
+redis['master_name'] = 'gitlab-redis' # must be the same in every sentinel node
+sentinel['bind'] = '10.0.0.2'
+# sentinel['port'] = 26379 # uncomment to change default port
+sentinel['quorum'] = 2
+# sentinel['down_after_milliseconds'] = 10000
+# sentinel['failover_timeout'] = 60000
+```
+
+[Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+
+### Example configuration for Redis slave 2 and Sentinel 3
+
+In `/etc/gitlab/gitlab.rb`:
+
+```ruby
+redis_slave_role['enable'] = true
+redis_sentinel_role['enable'] = true
+redis['bind'] = '10.0.0.3'
+redis['port'] = 6379
+redis['password'] = 'redis-password-goes-here'
+redis['master_password'] = 'redis-password-goes-here'
+redis['master_ip'] = '10.0.0.1' # IP of master Redis server
+#redis['master_port'] = 6379 # Port of master Redis server, uncomment to change to non default
+redis['master_name'] = 'gitlab-redis' # must be the same in every sentinel node
+sentinel['bind'] = '10.0.0.3'
+# sentinel['port'] = 26379 # uncomment to change default port
+sentinel['quorum'] = 2
+# sentinel['down_after_milliseconds'] = 10000
+# sentinel['failover_timeout'] = 60000
```
-When in doubt, please read [Redis Sentinel documentation](http://redis.io/topics/sentinel)
+[Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+
+### Example configuration for the GitLab application
+
+In `/etc/gitlab/gitlab.rb`:
+
+```ruby
+redis['master_name'] = 'gitlab-redis'
+redis['password'] = 'redis-password-goes-here'
+gitlab_rails['redis_sentinels'] = [
+ {'host' => '10.0.0.1', 'port' => 26379},
+ {'host' => '10.0.0.2', 'port' => 26379},
+ {'host' => '10.0.0.3', 'port' => 26379}
+]
+```
+
+[Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+
+## Advanced configuration
+
+Omnibus GitLab configures some things behind the curtains to make the sysadmins'
+lives easier. If you want to know what happens underneath keep reading.
+
+### Control running services
+
+In the previous example, we've used `redis_sentinel_role` and
+`redis_master_role` which simplifies the amount of configuration changes.
+
+If you want more control, here is what each one sets for you automatically
+when enabled:
+
+```ruby
+## Redis Sentinel Role
+redis_sentinel_role['enable'] = true
+
+# When Sentinel Role is enabled, the following services are also enabled
+sentinel['enable'] = true
+
+# The following services are disabled
+redis['enable'] = false
+bootstrap['enable'] = false
+nginx['enable'] = false
+postgresql['enable'] = false
+gitlab_rails['enable'] = false
+mailroom['enable'] = false
+
+-------
+
+## Redis master/slave Role
+redis_master_role['enable'] = true # enable only one of them
+redis_slave_role['enable'] = true # enable only one of them
+
+# When Redis Master or Slave role are enabled, the following services are
+# enabled/disabled. Note that if Redis and Sentinel roles are combined, both
+# services will be enabled.
+
+# The following services are disabled
+sentinel['enable'] = false
+bootstrap['enable'] = false
+nginx['enable'] = false
+postgresql['enable'] = false
+gitlab_rails['enable'] = false
+mailroom['enable'] = false
+
+# For Redis Slave role, also change this setting from default 'true' to 'false':
+redis['master'] = false
+```
+
+You can find the relevant attributes defined in [gitlab_rails.rb][omnifile].
+
+## Troubleshooting
+
+There are a lot of moving parts that needs to be taken care carefully
+in order for the HA setup to work as expected.
+
+Before proceeding with the troubleshooting below, check your firewall rules:
+
+- Redis machines
+ - Accept TCP connection in `6379`
+ - Connect to the other Redis machines via TCP in `6379`
+- Sentinel machines
+ - Accept TCP connection in `26379`
+ - Connect to other Sentinel machines via TCP in `26379`
+ - Connect to the Redis machines via TCP in `6379`
+
+### Troubleshooting Redis replication
+
+You can check if everything is correct by connecting to each server using
+`redis-cli` application, and sending the `INFO` command.
+
+If authentication was correctly defined, it should fail with:
+`NOAUTH Authentication required` error. Try to authenticate with the
+previous defined password with `AUTH redis-password-goes-here` and
+try the `INFO` command again.
+
+Look for the `# Replication` section where you should see some important
+information like the `role` of the server.
+
+When connected to a `master` redis, you will see the number of connected
+`slaves`, and a list of each with connection details:
+
+```
+# Replication
+role:master
+connected_slaves:1
+slave0:ip=10.133.5.21,port=6379,state=online,offset=208037514,lag=1
+master_repl_offset:208037658
+repl_backlog_active:1
+repl_backlog_size:1048576
+repl_backlog_first_byte_offset:206989083
+repl_backlog_histlen:1048576
+```
+
+When it's a `slave`, you will see details of the master connection and if
+its `up` or `down`:
+
+```
+# Replication
+role:slave
+master_host:10.133.1.58
+master_port:6379
+master_link_status:up
+master_last_io_seconds_ago:1
+master_sync_in_progress:0
+slave_repl_offset:208096498
+slave_priority:100
+slave_read_only:1
+connected_slaves:0
+master_repl_offset:0
+repl_backlog_active:0
+repl_backlog_size:1048576
+repl_backlog_first_byte_offset:0
+repl_backlog_histlen:0
+```
+
+### Troubleshooting Sentinel
+
+If you get an error like: `Redis::CannotConnectError: No sentinels available.`,
+there may be something wrong with your configuration files or it can be related
+to [this issue][gh-531].
+
+You must make sure you are defining the same value in `redis['master_name']`
+and `redis['master_pasword']` as you defined for your sentinel node.
+
+The way the redis connector `redis-rb` works with sentinel is a bit
+non-intuitive. We try to hide the complexity in omnibus, but it still requires
+a few extra configs.
---
@@ -273,7 +788,7 @@ To make sure your configuration is correct:
sudo gitlab-rails console
# For source installations
- sudo -u git rails console RAILS_ENV=production
+ sudo -u git rails console production
```
1. Run in the console:
@@ -288,8 +803,8 @@ To make sure your configuration is correct:
1. To simulate a failover on master Redis, SSH into the Redis server and run:
```bash
- # port must match your master redis port
- redis-cli -h localhost -p 6379 DEBUG sleep 60
+ # port must match your master redis port, and the sleep time must be a few seconds bigger than defined one
+ redis-cli -h localhost -p 6379 DEBUG sleep 20
```
1. Then back in the Rails console from the first step, run:
@@ -301,10 +816,26 @@ To make sure your configuration is correct:
You should see a different port after a few seconds delay
(the failover/reconnect time).
----
-Read more on high-availability configuration:
+## Changelog
+
+Changes to Redis HA over time.
+
+**8.14**
+
+- Redis Sentinel support is production-ready and bundled in the Omnibus GitLab
+ Enterprise Edition package
+- Documentation restructure for better readability
+
+**8.11**
+
+- Experimental Redis Sentinel support was added
+
+## Further reading
+
+Read more on High Availability:
+1. [High Availability Overview](README.md)
1. [Configure the database](database.md)
1. [Configure NFS](nfs.md)
1. [Configure the GitLab application servers](gitlab.md)
@@ -315,3 +846,10 @@ Read more on high-availability configuration:
[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
[gh-531]: https://github.com/redis/redis-rb/issues/531
[gh-534]: https://github.com/redis/redis-rb/issues/534
+[redis]: http://redis.io/
+[sentinel]: http://redis.io/topics/sentinel
+[omnifile]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/libraries/gitlab_rails.rb
+[source]: ../../install/installation.md
+[ce]: https://about.gitlab.com/downloads
+[ee]: https://about.gitlab.com/downloads-ee
+[it]: https://gitlab.com/gitlab-org/gitlab-ce/uploads/c4cc8cd353604bd80315f9384035ff9e/The_Internet_IT_Crowd.png
diff --git a/doc/administration/high_availability/redis_source.md b/doc/administration/high_availability/redis_source.md
new file mode 100644
index 00000000000..3629772b8af
--- /dev/null
+++ b/doc/administration/high_availability/redis_source.md
@@ -0,0 +1,366 @@
+# Configuring non-Omnibus Redis for GitLab HA
+
+This is the documentation for configuring a Highly Available Redis setup when
+you have installed Redis all by yourself and not using the bundled one that
+comes with the Omnibus packages.
+
+We cannot stress enough the importance of reading the
+[Overview section](redis.md#overview) of the Omnibus Redis HA as it provides
+some invaluable information to the configuration of Redis. Please proceed to
+read it before going forward with this guide.
+
+We also highly recommend that you use the Omnibus GitLab packages, as we
+optimize them specifically for GitLab, and we will take care of upgrading Redis
+to the latest supported version.
+
+If you're not sure whether this guide is for you, please refer to
+[Available configuration setups](redis.md#available-configuration-setups) in
+the Omnibus Redis HA documentation.
+
+## Configuring your own Redis server
+
+This is the section where we install and setup the new Redis instances.
+
+### Prerequisites
+
+- All Redis servers in this guide must be configured to use a TCP connection
+ instead of a socket. To configure Redis to use TCP connections you need to
+ define both `bind` and `port` in the Redis config file. You can bind to all
+ interfaces (`0.0.0.0`) or specify the IP of the desired interface
+ (e.g., one from an internal network).
+- Since Redis 3.2, you must define a password to receive external connections
+ (`requirepass`).
+- If you are using Redis with Sentinel, you will also need to define the same
+ password for the slave password definition (`masterauth`) in the same instance.
+
+In addition, read the prerequisites as described in the
+[Omnibus Redis HA document](redis.md#prerequisites) since they provide some
+valuable information for the general setup.
+
+### Step 1. Configuring the master Redis instance
+
+Assuming that the Redis master instance IP is `10.0.0.1`:
+
+1. [Install Redis](../../install/installation.md#6-redis)
+1. Edit `/etc/redis/redis.conf`:
+
+ ```conf
+ ## Define a `bind` address pointing to a local IP that your other machines
+ ## can reach you. If you really need to bind to an external accessible IP, make
+ ## sure you add extra firewall rules to prevent unauthorized access:
+ bind 10.0.0.1
+
+ ## Define a `port` to force redis to listen on TCP so other machines can
+ ## connect to it (default port is `6379`).
+ port 6379
+
+ ## Set up password authentication (use the same password in all nodes).
+ ## The password should be defined equal for both `requirepass` and `masterauth`
+ ## when setting up Redis to use with Sentinel.
+ requirepass redis-password-goes-here
+ masterauth redis-password-goes-here
+ ```
+
+1. Restart the Redis service for the changes to take effect.
+
+### Step 2. Configuring the slave Redis instances
+
+Assuming that the Redis slave instance IP is `10.0.0.2`:
+
+1. [Install Redis](../../install/installation.md#6-redis)
+1. Edit `/etc/redis/redis.conf`:
+
+ ```conf
+ ## Define a `bind` address pointing to a local IP that your other machines
+ ## can reach you. If you really need to bind to an external accessible IP, make
+ ## sure you add extra firewall rules to prevent unauthorized access:
+ bind 10.0.0.2
+
+ ## Define a `port` to force redis to listen on TCP so other machines can
+ ## connect to it (default port is `6379`).
+ port 6379
+
+ ## Set up password authentication (use the same password in all nodes).
+ ## The password should be defined equal for both `requirepass` and `masterauth`
+ ## when setting up Redis to use with Sentinel.
+ requirepass redis-password-goes-here
+ masterauth redis-password-goes-here
+
+ ## Define `slaveof` pointing to the Redis master instance with IP and port.
+ slaveof 10.0.0.1 6379
+ ```
+
+1. Restart the Redis service for the changes to take effect.
+1. Go through the steps again for all the other slave nodes.
+
+### Step 3. Configuring the Redis Sentinel instances
+
+Sentinel is a special type of Redis server. It inherits most of the basic
+configuration options you can define in `redis.conf`, with specific ones
+starting with `sentinel` prefix.
+
+Assuming that the Redis Sentinel is installed on the same instance as Redis
+master with IP `10.0.0.1` (some settings might overlap with the master):
+
+1. [Install Redis Sentinel](http://redis.io/topics/sentinel)
+1. Edit `/etc/redis/sentinel.conf`:
+
+ ```conf
+ ## Define a `bind` address pointing to a local IP that your other machines
+ ## can reach you. If you really need to bind to an external accessible IP, make
+ ## sure you add extra firewall rules to prevent unauthorized access:
+ bind 10.0.0.1
+
+ ## Define a `port` to force Sentinel to listen on TCP so other machines can
+ ## connect to it (default port is `6379`).
+ port 26379
+
+ ## Set up password authentication (use the same password in all nodes).
+ ## The password should be defined equal for both `requirepass` and `masterauth`
+ ## when setting up Redis to use with Sentinel.
+ requirepass redis-password-goes-here
+ masterauth redis-password-goes-here
+
+ ## Define with `sentinel auth-pass` the same shared password you have
+ ## defined for both Redis master and slaves instances.
+ sentinel auth-pass gitlab-redis redis-password-goes-here
+
+ ## Define with `sentinel monitor` the IP and port of the Redis
+ ## master node, and the quorum required to start a failover.
+ sentinel monitor gitlab-redis 10.0.0.1 6379 2
+
+ ## Define with `sentinel down-after-milliseconds` the time in `ms`
+ ## that an unresponsive server will be considered down.
+ sentinel down-after-milliseconds gitlab-redis 10000
+
+ ## Define a value for `sentinel failover_timeout` in `ms`. This has multiple
+ ## meanings:
+ ##
+ ## * The time needed to re-start a failover after a previous failover was
+ ## already tried against the same master by a given Sentinel, is two
+ ## times the failover timeout.
+ ##
+ ## * The time needed for a slave replicating to a wrong master according
+ ## to a Sentinel current configuration, to be forced to replicate
+ ## with the right master, is exactly the failover timeout (counting since
+ ## the moment a Sentinel detected the misconfiguration).
+ ##
+ ## * The time needed to cancel a failover that is already in progress but
+ ## did not produced any configuration change (SLAVEOF NO ONE yet not
+ ## acknowledged by the promoted slave).
+ ##
+ ## * The maximum time a failover in progress waits for all the slaves to be
+ ## reconfigured as slaves of the new master. However even after this time
+ ## the slaves will be reconfigured by the Sentinels anyway, but not with
+ ## the exact parallel-syncs progression as specified.
+ sentinel failover_timeout 30000
+ ```
+1. Restart the Redis service for the changes to take effect.
+1. Go through the steps again for all the other Sentinel nodes.
+
+### Step 4. Configuring the GitLab application
+
+You can enable or disable Sentinel support at any time in new or existing
+installations. From the GitLab application perspective, all it requires is
+the correct credentials for the Sentinel nodes.
+
+While it doesn't require a list of all Sentinel nodes, in case of a failure,
+it needs to access at least one of listed ones.
+
+The following steps should be performed in the [GitLab application server](gitlab.md)
+which ideally should not have Redis or Sentinels in the same machine for a HA
+setup:
+
+1. Edit `/home/git/gitlab/config/resque.yml` following the example in
+ [resque.yml.example][resque], and uncomment the Sentinel lines, pointing to
+ the correct server credentials:
+
+ ```yaml
+ # resque.yaml
+ production:
+ url: redis://:redi-password-goes-here@gitlab-redis/
+ sentinels:
+ -
+ host: 10.0.0.1
+ port: 26379 # point to sentinel, not to redis port
+ -
+ host: 10.0.0.2
+ port: 26379 # point to sentinel, not to redis port
+ -
+ host: 10.0.0.3
+ port: 26379 # point to sentinel, not to redis port
+ ```
+
+1. [Restart GitLab][restart] for the changes to take effect.
+
+## Example of minimal configuration with 1 master, 2 slaves and 3 Sentinels
+
+In this example we consider that all servers have an internal network
+interface with IPs in the `10.0.0.x` range, and that they can connect
+to each other using these IPs.
+
+In a real world usage, you would also setup firewall rules to prevent
+unauthorized access from other machines, and block traffic from the
+outside ([Internet][it]).
+
+For this example, **Sentinel 1** will be configured in the same machine as the
+**Redis Master**, **Sentinel 2** and **Sentinel 3** in the same machines as the
+**Slave 1** and **Slave 2** respectively.
+
+Here is a list and description of each **machine** and the assigned **IP**:
+
+* `10.0.0.1`: Redis Master + Sentinel 1
+* `10.0.0.2`: Redis Slave 1 + Sentinel 2
+* `10.0.0.3`: Redis Slave 2 + Sentinel 3
+* `10.0.0.4`: GitLab application
+
+Please note that after the initial configuration, if a failover is initiated
+by the Sentinel nodes, the Redis nodes will be reconfigured and the **Master**
+will change permanently (including in `redis.conf`) from one node to the other,
+until a new failover is initiated again.
+
+The same thing will happen with `sentinel.conf` that will be overridden after the
+initial execution, after any new sentinel node starts watching the **Master**,
+or a failover promotes a different **Master** node.
+
+### Example configuration for Redis master and Sentinel 1
+
+1. In `/etc/redis/redis.conf`:
+
+ ```conf
+ bind 10.0.0.1
+ port 6379
+ requirepass redis-password-goes-here
+ masterauth redis-password-goes-here
+ ```
+
+1. In `/etc/redis/sentinel.conf`:
+
+ ```conf
+ bind 10.0.0.1
+ port 26379
+ sentinel auth-pass gitlab-redis redis-password-goes-here
+ sentinel monitor gitlab-redis 10.0.0.1 6379 2
+ sentinel down-after-milliseconds gitlab-redis 10000
+ sentinel failover_timeout 30000
+ ```
+
+1. Restart the Redis service for the changes to take effect.
+
+### Example configuration for Redis slave 1 and Sentinel 2
+
+1. In `/etc/redis/redis.conf`:
+
+ ```conf
+ bind 10.0.0.2
+ port 6379
+ requirepass redis-password-goes-here
+ masterauth redis-password-goes-here
+ slaveof 10.0.0.1 6379
+ ```
+
+1. In `/etc/redis/sentinel.conf`:
+
+ ```conf
+ bind 10.0.0.2
+ port 26379
+ sentinel auth-pass gitlab-redis redis-password-goes-here
+ sentinel monitor gitlab-redis 10.0.0.1 6379 2
+ sentinel down-after-milliseconds gitlab-redis 10000
+ sentinel failover_timeout 30000
+ ```
+
+1. Restart the Redis service for the changes to take effect.
+
+### Example configuration for Redis slave 2 and Sentinel 3
+
+1. In `/etc/redis/redis.conf`:
+
+ ```conf
+ bind 10.0.0.3
+ port 6379
+ requirepass redis-password-goes-here
+ masterauth redis-password-goes-here
+ slaveof 10.0.0.1 6379
+ ```
+
+1. In `/etc/redis/sentinel.conf`:
+
+ ```conf
+ bind 10.0.0.3
+ port 26379
+ sentinel auth-pass gitlab-redis redis-password-goes-here
+ sentinel monitor gitlab-redis 10.0.0.1 6379 2
+ sentinel down-after-milliseconds gitlab-redis 10000
+ sentinel failover_timeout 30000
+ ```
+
+1. Restart the Redis service for the changes to take effect.
+
+### Example configuration of the GitLab application
+
+1. Edit `/home/git/gitlab/config/resque.yml`:
+
+ ```yaml
+ production:
+ url: redis://:redi-password-goes-here@gitlab-redis/
+ sentinels:
+ -
+ host: 10.0.0.1
+ port: 26379 # point to sentinel, not to redis port
+ -
+ host: 10.0.0.2
+ port: 26379 # point to sentinel, not to redis port
+ -
+ host: 10.0.0.3
+ port: 26379 # point to sentinel, not to redis port
+ ```
+
+1. [Restart GitLab][restart] for the changes to take effect.
+
+## Troubleshooting
+
+We have a more detailed [Troubleshooting](redis.md#troubleshooting) explained
+in the documentation for Omnibus GitLab installations. Here we will list only
+the things that are specific to a source installation.
+
+If you get an error in GitLab like `Redis::CannotConnectError: No sentinels available.`,
+there may be something wrong with your configuration files or it can be related
+to [this upstream issue][gh-531].
+
+You must make sure that `resque.yml` and `sentinel.conf` are configured correctly,
+otherwise `redis-rb` will not work properly.
+
+The `master-group-name` ('gitlab-redis') defined in (`sentinel.conf`)
+**must** be used as the hostname in GitLab (`resque.yml`):
+
+```conf
+# sentinel.conf:
+sentinel monitor gitlab-redis 10.0.0.1 6379 2
+sentinel down-after-milliseconds gitlab-redis 10000
+sentinel config-epoch gitlab-redis 0
+sentinel leader-epoch gitlab-redis 0
+```
+
+```yaml
+# resque.yaml
+production:
+ url: redis://:myredispassword@gitlab-redis/
+ sentinels:
+ -
+ host: 10.0.0.1
+ port: 26379 # point to sentinel, not to redis port
+ -
+ host: 10.0.0.2
+ port: 26379 # point to sentinel, not to redis port
+ -
+ host: 10.0.0.3
+ port: 26379 # point to sentinel, not to redis port
+```
+
+When in doubt, please read [Redis Sentinel documentation](http://redis.io/topics/sentinel).
+
+[gh-531]: https://github.com/redis/redis-rb/issues/531
+[downloads]: https://about.gitlab.com/downloads
+[restart]: ../restart_gitlab.md#installations-from-source
+[it]: https://gitlab.com/gitlab-org/gitlab-ce/uploads/c4cc8cd353604bd80315f9384035ff9e/The_Internet_IT_Crowd.png
diff --git a/doc/administration/img/custom_hooks_error_msg.png b/doc/administration/img/custom_hooks_error_msg.png
index 92e87e15fb3..1b3277bef16 100644
--- a/doc/administration/img/custom_hooks_error_msg.png
+++ b/doc/administration/img/custom_hooks_error_msg.png
Binary files differ
diff --git a/doc/administration/img/high_availability/active-active-diagram.png b/doc/administration/img/high_availability/active-active-diagram.png
index 81259e0ae93..4f5984b88fe 100644
--- a/doc/administration/img/high_availability/active-active-diagram.png
+++ b/doc/administration/img/high_availability/active-active-diagram.png
Binary files differ
diff --git a/doc/administration/img/high_availability/active-passive-diagram.png b/doc/administration/img/high_availability/active-passive-diagram.png
index f69ff1d0357..3b42ce5911c 100644
--- a/doc/administration/img/high_availability/active-passive-diagram.png
+++ b/doc/administration/img/high_availability/active-passive-diagram.png
Binary files differ
diff --git a/doc/administration/img/housekeeping_settings.png b/doc/administration/img/housekeeping_settings.png
index 6ebc6205635..acc4506993a 100644
--- a/doc/administration/img/housekeeping_settings.png
+++ b/doc/administration/img/housekeeping_settings.png
Binary files differ
diff --git a/doc/administration/img/raketasks/check_repos_output.png b/doc/administration/img/raketasks/check_repos_output.png
new file mode 100644
index 00000000000..7fda2ba0c0f
--- /dev/null
+++ b/doc/administration/img/raketasks/check_repos_output.png
Binary files differ
diff --git a/doc/administration/img/repository_storages_admin_ui.png b/doc/administration/img/repository_storages_admin_ui.png
index 6481baca1ad..3e76c5b282c 100644
--- a/doc/administration/img/repository_storages_admin_ui.png
+++ b/doc/administration/img/repository_storages_admin_ui.png
Binary files differ
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index d757a3c2a66..4b8d5c5cc87 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -1,4 +1,4 @@
-## Log system
+# Log system
GitLab has an advanced log system where everything is logged so that you
can analyze your instance using various system log files. In addition to
@@ -9,10 +9,10 @@ documentation](http://docs.gitlab.com/ee/administration/audit_events.html)
System log files are typically plain text in a standard log file format.
This guide talks about how to read and use these system log files.
-### production.log
+## `production.log`
This file lives in `/var/log/gitlab/gitlab-rails/production.log` for
-omnibus package or in `/home/git/gitlab/log/production.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/production.log` for
installations from source. (When Gitlab is running in an environment
other than production, the corresponding logfile is shown here.)
@@ -46,10 +46,10 @@ In this example we can see that server processed an HTTP request with URL
19:34:53 +0200. Also we can see that request was processed by
`Projects::TreeController`.
-### application.log
+## `application.log`
This file lives in `/var/log/gitlab/gitlab-rails/application.log` for
-omnibus package or in `/home/git/gitlab/log/application.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/application.log` for
installations from source.
It helps you discover events happening in your instance such as user creation,
@@ -63,10 +63,10 @@ October 07, 2014 11:25: User "Claudie Hodkiewicz" (nasir_stehr@olson.co.uk) was
October 07, 2014 11:25: Project "project133" was removed
```
-### githost.log
+## `githost.log`
This file lives in `/var/log/gitlab/gitlab-rails/githost.log` for
-omnibus package or in `/home/git/gitlab/log/githost.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/githost.log` for
installations from source.
GitLab has to interact with Git repositories but in some rare cases
@@ -81,10 +81,10 @@ December 03, 2014 13:20 -> ERROR -> Command failed [1]: /usr/bin/git --git-dir=/
error: failed to push some refs to '/Users/vsizov/gitlab-development-kit/repositories/gitlabhq/gitlab_git.git'
```
-### sidekiq.log
+## `sidekiq.log`
This file lives in `/var/log/gitlab/gitlab-rails/sidekiq.log` for
-omnibus package or in `/home/git/gitlab/log/sidekiq.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/sidekiq.log` for
installations from source.
GitLab uses background jobs for processing tasks which can take a long
@@ -96,10 +96,10 @@ this file. For example:
2014-06-10T18:18:26Z 14299 TID-55uqo INFO: Booting Sidekiq 3.0.0 with redis options {:url=>"redis://localhost:6379/0", :namespace=>"sidekiq"}
```
-### gitlab-shell.log
+## `gitlab-shell.log`
This file lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log` for
-omnibus package or in `/home/git/gitlab-shell/gitlab-shell.log` for
+Omnibus GitLab packages or in `/home/git/gitlab-shell/gitlab-shell.log` for
installations from source.
GitLab shell is used by Gitlab for executing Git commands and provide
@@ -110,10 +110,10 @@ I, [2015-02-13T06:17:00.671315 #9291] INFO -- : Adding project root/example.git
I, [2015-02-13T06:17:00.679433 #9291] INFO -- : Moving existing hooks directory and symlinking global hooks directory for /var/opt/gitlab/git-data/repositories/root/example.git.
```
-### unicorn\_stderr.log
+## `unicorn\_stderr.log`
This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` for
-omnibus package or in `/home/git/gitlab/log/unicorn_stderr.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/unicorn_stderr.log` for
installations from source.
Unicorn is a high-performance forking Web server which is used for
@@ -136,3 +136,13 @@ I, [2015-02-13T07:16:01.530733 #9047] INFO -- : reaped #<Process::Status: pid 9
I, [2015-02-13T07:16:01.534501 #13379] INFO -- : worker=1 spawned pid=13379
I, [2015-02-13T07:16:01.534848 #13379] INFO -- : worker=1 ready
```
+
+## `repocheck.log`
+
+This file lives in `/var/log/gitlab/gitlab-rails/repocheck.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/repocheck.log` for
+installations from source.
+
+It logs information whenever a [repository check is run][repocheck] on a project.
+
+[repocheck]: repository_checks.md
diff --git a/doc/administration/monitoring/performance/img/grafana_dashboard_dropdown.png b/doc/administration/monitoring/performance/img/grafana_dashboard_dropdown.png
index 7e34fad71ce..51eef90068d 100644
--- a/doc/administration/monitoring/performance/img/grafana_dashboard_dropdown.png
+++ b/doc/administration/monitoring/performance/img/grafana_dashboard_dropdown.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/grafana_dashboard_import.png b/doc/administration/monitoring/performance/img/grafana_dashboard_import.png
index f97624365c7..7761ea00522 100644
--- a/doc/administration/monitoring/performance/img/grafana_dashboard_import.png
+++ b/doc/administration/monitoring/performance/img/grafana_dashboard_import.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/grafana_data_source_configuration.png b/doc/administration/monitoring/performance/img/grafana_data_source_configuration.png
index 7d50e4c88c2..3e749eb8f9d 100644
--- a/doc/administration/monitoring/performance/img/grafana_data_source_configuration.png
+++ b/doc/administration/monitoring/performance/img/grafana_data_source_configuration.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/grafana_data_source_empty.png b/doc/administration/monitoring/performance/img/grafana_data_source_empty.png
index aa39a53acae..33fcaaaef64 100644
--- a/doc/administration/monitoring/performance/img/grafana_data_source_empty.png
+++ b/doc/administration/monitoring/performance/img/grafana_data_source_empty.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/grafana_save_icon.png b/doc/administration/monitoring/performance/img/grafana_save_icon.png
index c740e33cd1c..c18f2147e9d 100644
--- a/doc/administration/monitoring/performance/img/grafana_save_icon.png
+++ b/doc/administration/monitoring/performance/img/grafana_save_icon.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/metrics_gitlab_configuration_settings.png b/doc/administration/monitoring/performance/img/metrics_gitlab_configuration_settings.png
index db396423e30..13bfd097b81 100644
--- a/doc/administration/monitoring/performance/img/metrics_gitlab_configuration_settings.png
+++ b/doc/administration/monitoring/performance/img/metrics_gitlab_configuration_settings.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/request_profile_result.png b/doc/administration/monitoring/performance/img/request_profile_result.png
index 73e2fdcab67..8ebd74c2d3c 100644
--- a/doc/administration/monitoring/performance/img/request_profile_result.png
+++ b/doc/administration/monitoring/performance/img/request_profile_result.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/request_profiling_token.png b/doc/administration/monitoring/performance/img/request_profiling_token.png
index 04d87567816..9160407e028 100644
--- a/doc/administration/monitoring/performance/img/request_profiling_token.png
+++ b/doc/administration/monitoring/performance/img/request_profiling_token.png
Binary files differ
diff --git a/doc/administration/operations.md b/doc/administration/operations.md
index 4b582d16b64..0daceb98d99 100644
--- a/doc/administration/operations.md
+++ b/doc/administration/operations.md
@@ -1,6 +1,7 @@
# GitLab operations
- [Sidekiq MemoryKiller](operations/sidekiq_memory_killer.md)
+- [Sidekiq Job throttling](operations/sidekiq_job_throttling.md)
- [Cleaning up Redis sessions](operations/cleaning_up_redis_sessions.md)
- [Understanding Unicorn and unicorn-worker-killer](operations/unicorn.md)
- [Moving repositories to a new location](operations/moving_repositories.md)
diff --git a/doc/administration/operations/img/sidekiq_job_throttling.png b/doc/administration/operations/img/sidekiq_job_throttling.png
new file mode 100644
index 00000000000..dcf40b4bf17
--- /dev/null
+++ b/doc/administration/operations/img/sidekiq_job_throttling.png
Binary files differ
diff --git a/doc/administration/operations/sidekiq_job_throttling.md b/doc/administration/operations/sidekiq_job_throttling.md
new file mode 100644
index 00000000000..ddeaa22e288
--- /dev/null
+++ b/doc/administration/operations/sidekiq_job_throttling.md
@@ -0,0 +1,33 @@
+# Sidekiq Job throttling
+
+> Note: Introduced with GitLab 8.14
+
+When your GitLab installation needs to handle tens of thousands of background
+jobs, it can be convenient to throttle queues that do not need to be executed
+immediately, e.g. long running jobs like Pipelines, thus allowing jobs that do
+need to be executed immediately to have access to more resources.
+
+In order to accomplish this, you can limit the amount of workers that certain
+slow running queues can have available. This is what we call Sidekiq Job
+Throttling. Depending on your infrastructure, you might have different slow
+running queues, which is why you can choose which queues you want to throttle
+and by how much you want to throttle them.
+
+These settings are available in the Application Settings of your GitLab
+installation.
+
+![Sidekiq Job Throttling](img/sidekiq_job_throttling.png)
+
+The throttle factor determines the maximum number of workers a queue can run on.
+This value gets multiplied by `:concurrency` value set in the Sidekiq settings
+and rounded up to the closest full integer.
+
+So, for example, you set the `:concurrency` to 25 and the `Throttling factor` to
+0.1, the maximum workers assigned to the selected queues would be 3.
+
+```ruby
+queue_limit = (factor * Sidekiq.options[:concurrency]).ceil
+```
+
+After enabling the job throttling, you will need to restart your GitLab
+instance, in order for the changes to take effect. \ No newline at end of file
diff --git a/doc/administration/raketasks/check.md b/doc/administration/raketasks/check.md
new file mode 100644
index 00000000000..d1d2fed4861
--- /dev/null
+++ b/doc/administration/raketasks/check.md
@@ -0,0 +1,97 @@
+# Check Rake Tasks
+
+## Repository Integrity
+
+Even though Git is very resilient and tries to prevent data integrity issues,
+there are times when things go wrong. The following Rake tasks intend to
+help GitLab administrators diagnose problem repositories so they can be fixed.
+
+There are 3 things that are checked to determine integrity.
+
+1. Git repository file system check ([git fsck](https://git-scm.com/docs/git-fsck)).
+ This step verifies the connectivity and validity of objects in the repository.
+1. Check for `config.lock` in the repository directory.
+1. Check for any branch/references lock files in `refs/heads`.
+
+It's important to note that the existence of `config.lock` or reference locks
+alone do not necessarily indicate a problem. Lock files are routinely created
+and removed as Git and GitLab perform operations on the repository. They serve
+to prevent data integrity issues. However, if a Git operation is interrupted these
+locks may not be cleaned up properly.
+
+The following symptoms may indicate a problem with repository integrity. If users
+experience these symptoms you may use the rake tasks described below to determine
+exactly which repositories are causing the trouble.
+
+- Receiving an error when trying to push code - `remote: error: cannot lock ref`
+- A 500 error when viewing the GitLab dashboard or when accessing a specific project.
+
+### Check all GitLab repositories
+
+This task loops through all repositories on the GitLab server and runs the
+3 integrity checks described previously.
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake gitlab:repo:check
+```
+
+**Source Installation**
+
+```bash
+sudo -u git -H bundle exec rake gitlab:repo:check RAILS_ENV=production
+```
+
+### Check repositories for a specific user
+
+This task checks all repositories that a specific user has access to. This is important
+because sometimes you know which user is experiencing trouble but you don't know
+which project might be the cause.
+
+If the rake task is executed without brackets at the end, you will be prompted
+to enter a username.
+
+**Omnibus Installation**
+
+```bash
+sudo gitlab-rake gitlab:user:check_repos
+sudo gitlab-rake gitlab:user:check_repos[<username>]
+```
+
+**Source Installation**
+
+```bash
+sudo -u git -H bundle exec rake gitlab:user:check_repos RAILS_ENV=production
+sudo -u git -H bundle exec rake gitlab:user:check_repos[<username>] RAILS_ENV=production
+```
+
+Example output:
+
+![gitlab:user:check_repos output](../img/raketasks/check_repos_output.png)
+
+## LDAP Check
+
+The LDAP check Rake task will test the bind_dn and password credentials
+(if configured) and will list a sample of LDAP users. This task is also
+executed as part of the `gitlab:check` task, but can run independently
+using the command below.
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake gitlab:ldap:check
+```
+
+**Source Installation**
+
+```bash
+sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production
+```
+
+By default, the task will return a sample of 100 LDAP users. Change this
+limit by passing a number to the check task:
+
+```bash
+rake gitlab:ldap:check[50]
+```
diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md
index 5a9a1582877..14cd7a03826 100644
--- a/doc/administration/reply_by_email.md
+++ b/doc/administration/reply_by_email.md
@@ -105,6 +105,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
# The mailbox where incoming mail will end up. Usually "inbox".
gitlab_rails['incoming_email_mailbox_name'] = "inbox"
+ # The IDLE command timeout.
+ gitlab_rails['incoming_email_idle_timeout'] = 60
```
```ruby
@@ -133,6 +135,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
# The mailbox where incoming mail will end up. Usually "inbox".
gitlab_rails['incoming_email_mailbox_name'] = "inbox"
+ # The IDLE command timeout.
+ gitlab_rails['incoming_email_idle_timeout'] = 60
```
1. Reconfigure GitLab and restart mailroom for the changes to take effect:
@@ -192,6 +196,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
# The mailbox where incoming mail will end up. Usually "inbox".
mailbox: "inbox"
+ # The IDLE command timeout.
+ idle_timeout: 60
```
```yaml
@@ -221,6 +227,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
# The mailbox where incoming mail will end up. Usually "inbox".
mailbox: "inbox"
+ # The IDLE command timeout.
+ idle_timeout: 60
```
1. Enable `mail_room` in the init script at `/etc/default/gitlab`:
@@ -277,6 +285,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
# The mailbox where incoming mail will end up. Usually "inbox".
mailbox: "inbox"
+ # The IDLE command timeout.
+ idle_timeout: 60
```
As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-incoming@gmail.com`.
diff --git a/doc/api/access_requests.md b/doc/api/access_requests.md
index ea308b54d62..dee3e384080 100644
--- a/doc/api/access_requests.md
+++ b/doc/api/access_requests.md
@@ -18,8 +18,6 @@
Gets a list of access requests viewable by the authenticated user.
-Returns `200` if the request succeeds.
-
```
GET /groups/:id/access_requests
GET /projects/:id/access_requests
@@ -61,8 +59,6 @@ Example response:
Requests access for the authenticated user to a group or project.
-Returns `201` if the request succeeds.
-
```
POST /groups/:id/access_requests
POST /projects/:id/access_requests
@@ -94,8 +90,6 @@ Example response:
Approves an access request for the given user.
-Returns `201` if the request succeeds.
-
```
PUT /groups/:id/access_requests/:user_id/approve
PUT /projects/:id/access_requests/:user_id/approve
@@ -129,8 +123,6 @@ Example response:
Denies an access request for the given user.
-Returns `200` if the request succeeds.
-
```
DELETE /groups/:id/access_requests/:user_id
DELETE /projects/:id/access_requests/:user_id
diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md
index 06111f4ab67..58092bdd400 100644
--- a/doc/api/award_emoji.md
+++ b/doc/api/award_emoji.md
@@ -158,7 +158,7 @@ Example Response:
### Delete an award emoji
Sometimes its just not meant to be, and you'll have to remove your award. Only available to
-admins or the author of the award. Status code 200 on success, 401 if unauthorized.
+admins or the author of the award.
```
DELETE /projects/:id/issues/:issue_id/award_emoji/:award_id
@@ -331,7 +331,7 @@ Example Response:
### Delete an award emoji
Sometimes its just not meant to be, and you'll have to remove your award. Only available to
-admins or the author of the award. Status code 200 on success, 401 if unauthorized.
+admins or the author of the award.
```
DELETE /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id
diff --git a/doc/api/boards.md b/doc/api/boards.md
index 28681719f43..c83db6df80c 100644
--- a/doc/api/boards.md
+++ b/doc/api/boards.md
@@ -148,10 +148,6 @@ Example response:
Creates a new Issue Board list.
-If the operation is successful, a status code of `200` and the newly-created
-list is returned. If an error occurs, an error number and a message explaining
-the reason is returned.
-
```
POST /projects/:id/boards/:board_id/lists
```
@@ -184,10 +180,6 @@ Example response:
Updates an existing Issue Board list. This call is used to change list position.
-If the operation is successful, a code of `200` and the updated board list is
-returned. If an error occurs, an error number and a message explaining the
-reason is returned.
-
```
PUT /projects/:id/boards/:board_id/lists/:list_id
```
@@ -220,8 +212,6 @@ Example response:
## Delete a board list
Only for admins and project owners. Soft deletes the board list in question.
-If the operation is successful, a status code `200` is returned. In case you cannot
-destroy this board list, or it is not present, code `404` is given.
```
DELETE /projects/:id/boards/:board_id/lists/:list_id
diff --git a/doc/api/branches.md b/doc/api/branches.md
index 0b5f7778fc7..ffcfea41453 100644
--- a/doc/api/branches.md
+++ b/doc/api/branches.md
@@ -22,6 +22,7 @@ Example response:
[
{
"name": "master",
+ "merged": false,
"protected": true,
"developers_can_push": false,
"developers_can_merge": false,
@@ -65,6 +66,7 @@ Example response:
```json
{
"name": "master",
+ "merged": false,
"protected": true,
"developers_can_push": false,
"developers_can_merge": false,
@@ -123,6 +125,7 @@ Example response:
]
},
"name": "master",
+ "merged": false,
"protected": true,
"developers_can_push": true,
"developers_can_merge": true
@@ -166,6 +169,7 @@ Example response:
]
},
"name": "master",
+ "merged": false,
"protected": false,
"developers_can_push": false,
"developers_can_merge": false
@@ -206,15 +210,13 @@ Example response:
]
},
"name": "newbranch",
+ "merged": false,
"protected": false,
"developers_can_push": false,
"developers_can_merge": false
}
```
-It returns `200` if it succeeds or `400` if failed with an error message
-explaining the reason.
-
## Delete repository branch
```
@@ -226,8 +228,7 @@ DELETE /projects/:id/repository/branches/:branch
| `id` | integer | yes | The ID of a project |
| `branch` | string | yes | The name of the branch |
-It returns `200` if it succeeds, `404` if the branch to be deleted does not exist
-or `400` for other reasons. In case of an error, an explaining message is provided.
+In case of an error, an explaining message is provided.
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches/newbranch"
@@ -240,3 +241,20 @@ Example response:
"branch_name": "newbranch"
}
```
+
+## Delete merged branches
+
+Will delete all branches that are merged into the project's default branch.
+
+```
+DELETE /projects/:id/repository/merged_branches
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/merged_branches"
+```
diff --git a/doc/api/broadcast_messages.md b/doc/api/broadcast_messages.md
index c3a9207a3ae..a3e9c01f335 100644
--- a/doc/api/broadcast_messages.md
+++ b/doc/api/broadcast_messages.md
@@ -62,10 +62,6 @@ Example response:
## Create a broadcast message
-Responds with `400 Bad request` when the `message` parameter is missing or the
-`color` or `font` values are invalid, and `201 Created` when the broadcast
-message was successfully created.
-
```
POST /broadcast_messages
```
diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md
index 1b7a1840138..b6459971420 100644
--- a/doc/api/build_triggers.md
+++ b/doc/api/build_triggers.md
@@ -15,7 +15,7 @@ GET /projects/:id/triggers
| `id` | integer | yes | The ID of a project |
```
-curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers"
```
```json
@@ -51,7 +51,7 @@ GET /projects/:id/triggers/:token
| `token` | string | yes | The `token` of a trigger |
```
-curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
```
```json
@@ -77,7 +77,7 @@ POST /projects/:id/triggers
| `id` | integer | yes | The ID of a project |
```
-curl --request POST --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers"
```
```json
@@ -104,7 +104,7 @@ DELETE /projects/:id/triggers/:token
| `token` | string | yes | The `token` of a trigger |
```
-curl --request DELETE --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
```
```json
diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md
index a21751a49ea..917e9773913 100644
--- a/doc/api/build_variables.md
+++ b/doc/api/build_variables.md
@@ -13,7 +13,7 @@ GET /projects/:id/variables
| `id` | integer | yes | The ID of a project |
```
-curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables"
```
```json
@@ -43,7 +43,7 @@ GET /projects/:id/variables/:key
| `key` | string | yes | The `key` of a variable |
```
-curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1"
```
```json
@@ -68,7 +68,7 @@ POST /projects/:id/variables
| `value` | string | yes | The `value` of a variable |
```
-curl --request POST --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value"
```
```json
@@ -93,7 +93,7 @@ PUT /projects/:id/variables/:key
| `value` | string | yes | The `value` of a variable |
```
-curl --request PUT --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" --form "value=updated value"
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" --form "value=updated value"
```
```json
@@ -117,7 +117,7 @@ DELETE /projects/:id/variables/:key
| `key` | string | yes | The `key` of a variable |
```
-curl --request DELETE --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1"
```
```json
diff --git a/doc/api/builds.md b/doc/api/builds.md
index 0476cac0eda..bca2f9e44ef 100644
--- a/doc/api/builds.md
+++ b/doc/api/builds.md
@@ -45,7 +45,7 @@ Example of response
"ref": "master",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
- }
+ },
"ref": "master",
"runner": null,
"stage": "test",
@@ -89,7 +89,7 @@ Example of response
"ref": "master",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
- }
+ },
"ref": "master",
"runner": null,
"stage": "test",
@@ -163,7 +163,7 @@ Example of response
"ref": "master",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
- }
+ },
"ref": "master",
"runner": null,
"stage": "test",
@@ -193,7 +193,7 @@ Example of response
"ref": "master",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
- }
+ },
"ref": "master",
"runner": null,
"stage": "test",
@@ -260,7 +260,7 @@ Example of response
"ref": "master",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
- }
+ },
"ref": "master",
"runner": null,
"stage": "test",
diff --git a/doc/api/commits.md b/doc/api/commits.md
index e1ed99d98d3..5c11d0f83bb 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -29,6 +29,8 @@ Example response:
"title": "Replace sanitize with escape once",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dzaporozhets@sphereconsultinginc.com",
+ "committer_name": "Administrator",
+ "committer_email": "admin@example.com",
"created_at": "2012-09-20T11:50:22+03:00",
"message": "Replace sanitize with escape once",
"allow_failure": false
@@ -39,6 +41,8 @@ Example response:
"title": "Sanitize for network graph",
"author_name": "randx",
"author_email": "dmitriy.zaporozhets@gmail.com",
+ "committer_name": "Dmitriy",
+ "committer_email": "dmitriy.zaporozhets@gmail.com",
"created_at": "2012-09-20T09:06:12+03:00",
"message": "Sanitize for network graph",
"allow_failure": false
@@ -115,6 +119,8 @@ Example response:
"title": "some commit message",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dzaporozhets@sphereconsultinginc.com",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dzaporozhets@sphereconsultinginc.com",
"created_at": "2016-09-20T09:26:24.000-07:00",
"message": "some commit message",
"parent_ids": [
@@ -159,6 +165,8 @@ Example response:
"title": "Sanitize for network graph",
"author_name": "randx",
"author_email": "dmitriy.zaporozhets@gmail.com",
+ "committer_name": "Dmitriy",
+ "committer_email": "dmitriy.zaporozhets@gmail.com",
"created_at": "2012-09-20T09:06:12+03:00",
"message": "Sanitize for network graph",
"committed_date": "2012-09-20T09:06:12+03:00",
@@ -175,6 +183,44 @@ Example response:
}
```
+## Cherry pick a commit
+
+> [Introduced][ce-8047] in GitLab 8.15.
+
+Cherry picks a commit to a given branch.
+
+```
+POST /projects/:id/repository/commits/:sha/cherry_pick
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
+| `sha` | string | yes | The commit hash |
+| `branch` | string | yes | The name of the branch |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "branch=master" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/cherry_pick"
+```
+
+Example response:
+
+```json
+{
+ "id": "8b090c1b79a14f2bd9e8a738f717824ff53aebad",
+ "short_id": "8b090c1b",
+ "title": "Feature added",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "created_at": "2016-12-12T20:10:39.000+01:00",
+ "committer_name": "Administrator",
+ "committer_email": "admin@example.com",
+ "message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n"
+}
+```
+
## Get the diff of a commit
Get the diff of a commit in a project.
@@ -430,3 +476,4 @@ Example response:
```
[ce-6096]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6096 "Multi-file commit"
+[ce-8047]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8047
diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md
index 87a5fa67124..1299aca8c45 100644
--- a/doc/api/enviroments.md
+++ b/doc/api/enviroments.md
@@ -22,8 +22,9 @@ Example response:
[
{
"id": 1,
- "name": "Env1",
- "external_url": "https://env1.example.gitlab.com"
+ "name": "review/fix-foo",
+ "slug": "review-fix-foo-dfjre3",
+ "external_url": "https://review-fix-foo-dfjre3.example.gitlab.com"
}
]
```
@@ -54,6 +55,7 @@ Example response:
{
"id": 1,
"name": "deploy",
+ "slug": "deploy",
"external_url": "https://deploy.example.gitlab.com"
}
```
@@ -85,6 +87,7 @@ Example response:
{
"id": 1,
"name": "staging",
+ "slug": "staging",
"external_url": "https://staging.example.gitlab.com"
}
```
@@ -112,6 +115,7 @@ Example response:
{
"id": 1,
"name": "deploy",
+ "slug": "deploy",
"external_url": "https://deploy.example.gitlab.com"
}
```
diff --git a/doc/api/groups.md b/doc/api/groups.md
index b56d74d25e0..134d7bda22f 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -6,8 +6,13 @@ Get a list of groups. (As user: my groups or all available, as admin: all groups
Parameters:
-- `all_available` (optional) - if passed, show all groups you have access to
-- `skip_groups` (optional)(array of group IDs) - if passed, skip groups
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `skip_groups` | array of integers | no | Skip the group IDs passes |
+| `all_available` | boolean | no | Show all the groups you have access to |
+| `search` | string | no | Return list of authorized groups matching the search criteria |
+| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
+| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
```
GET /groups
@@ -26,6 +31,15 @@ GET /groups
You can search for groups by name or path, see below.
+=======
+## List owned groups
+
+Get a list of groups which are owned by the authenticated user.
+
+```
+GET /groups/owned
+```
+
## List a group's projects
Get a list of projects in this group.
@@ -36,12 +50,17 @@ GET /groups/:id/projects
Parameters:
-- `archived` (optional) - if passed, limit by archived status
-- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
-- `search` (optional) - Return list of authorized projects according to a search criteria
-- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or path of a group |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of authorized projects matching the search criteria |
+| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
+
+Example response:
```json
[
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 134263d27b4..119125bcd3d 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -315,10 +315,6 @@ Example response:
Creates a new project issue.
-If the operation is successful, a status code of `200` and the newly-created
-issue is returned. If an error occurs, an error number and a message explaining
-the reason is returned.
-
```
POST /projects/:id/issues
```
@@ -334,6 +330,7 @@ POST /projects/:id/issues
| `labels` | string | no | Comma-separated label names for an issue |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
+| `merge_request_for_resolving_discussions` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values. |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug
@@ -377,10 +374,6 @@ Example response:
Updates an existing project issue. This call is also used to mark an issue as
closed.
-If the operation is successful, a code of `200` and the updated issue is
-returned. If an error occurs, an error number and a message explaining the
-reason is returned.
-
```
PUT /projects/:id/issues/:issue_id
```
@@ -439,8 +432,6 @@ Example response:
## Delete an issue
Only for admins and project owners. Soft deletes the issue in question.
-If the operation is successful, a status code `200` is returned. In case you cannot
-destroy this issue, or it is not present, code `404` is given.
```
DELETE /projects/:id/issues/:issue_id
@@ -457,9 +448,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://git
## Move an issue
-Moves an issue to a different project. If the operation is successful, a status
-code `201` together with moved issue is returned. If the project, issue, or
-target project is not found, error `404` is returned. If the target project
+Moves an issue to a different project. If the target project
equals the source project or the user has insufficient permissions to move an
issue, error `400` together with an explaining error message is returned.
@@ -518,11 +507,9 @@ Example response:
## Subscribe to an issue
-Subscribes the authenticated user to an issue to receive notifications. If the
-operation is successful, status code `201` together with the updated issue is
-returned. If the user is already subscribed to the issue, the status code `304`
-is returned. If the project or issue is not found, status code `404` is
-returned.
+Subscribes the authenticated user to an issue to receive notifications.
+If the user is already subscribed to the issue, the status code `304`
+is returned.
```
POST /projects/:id/issues/:issue_id/subscription
@@ -576,10 +563,8 @@ Example response:
## Unsubscribe from an issue
Unsubscribes the authenticated user from the issue to not receive notifications
-from it. If the operation is successful, status code `200` together with the
-updated issue is returned. If the user is not subscribed to the issue, the
-status code `304` is returned. If the project or issue is not found, status code
-`404` is returned.
+from it. If the user is not subscribed to the issue, the
+status code `304` is returned.
```
DELETE /projects/:id/issues/:issue_id/subscription
@@ -633,8 +618,7 @@ Example response:
## Create a todo
-Manually creates a todo for the current user on an issue. If the request is
-successful, status code `200` together with the created todo is returned. If
+Manually creates a todo for the current user on an issue. If
there already exists a todo for the user on that issue, status code `304` is
returned.
diff --git a/doc/api/labels.md b/doc/api/labels.md
index 78686fdcad4..863b28c23b7 100644
--- a/doc/api/labels.md
+++ b/doc/api/labels.md
@@ -82,9 +82,6 @@ Example response:
Creates a new label for the given repository with the given name and color.
-It returns 200 if the label was successfully created, 400 for wrong parameters
-and 409 if the label already exists.
-
```
POST /projects/:id/labels
```
@@ -121,10 +118,6 @@ Example response:
Deletes a label with a given name.
-It returns 200 if the label was successfully deleted, 400 for wrong parameters
-and 404 if the label does not exist.
-In case of an error, an additional error message is returned.
-
```
DELETE /projects/:id/labels
```
@@ -159,10 +152,6 @@ Example response:
Updates an existing label with new name or new color. At least one parameter
is required, to update the label.
-It returns 200 if the label was successfully deleted, 400 for wrong parameters
-and 404 if the label does not exist.
-In case of an error, an additional error message is returned.
-
```
PUT /projects/:id/labels
```
@@ -199,11 +188,9 @@ Example response:
## Subscribe to a label
-Subscribes the authenticated user to a label to receive notifications. If the
-operation is successful, status code `201` together with the updated label is
-returned. If the user is already subscribed to the label, the status code `304`
-is returned. If the project or label is not found, status code `404` is
-returned.
+Subscribes the authenticated user to a label to receive notifications.
+If the user is already subscribed to the label, the status code `304`
+is returned.
```
POST /projects/:id/labels/:label_id/subscription
@@ -237,10 +224,8 @@ Example response:
## Unsubscribe from a label
Unsubscribes the authenticated user from a label to not receive notifications
-from it. If the operation is successful, status code `200` together with the
-updated label is returned. If the user is not subscribed to the label, the
-status code `304` is returned. If the project or label is not found, status code
-`404` is returned.
+from it. If the user is not subscribed to the label, the
+status code `304` is returned.
```
DELETE /projects/:id/labels/:label_id/subscription
diff --git a/doc/api/members.md b/doc/api/members.md
index 6535e9a7801..5dcb2a5f60a 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -16,8 +16,6 @@ The access levels are defined in the `Gitlab::Access` module. Currently, these l
Gets a list of group or project members viewable by the authenticated user.
-Returns `200` if the request succeeds.
-
```
GET /groups/:id/members
GET /projects/:id/members
@@ -60,8 +58,6 @@ Example response:
Gets a member of a group or project.
-Returns `200` if the request succeeds.
-
```
GET /groups/:id/members/:user_id
GET /projects/:id/members/:user_id
@@ -95,8 +91,6 @@ Example response:
Adds a member to a group or project.
-Returns `201` if the request succeeds.
-
```
POST /groups/:id/members
POST /projects/:id/members
@@ -131,8 +125,6 @@ Example response:
Updates a member of a group or project.
-Returns `200` if the request succeeds.
-
```
PUT /groups/:id/members/:user_id
PUT /projects/:id/members/:user_id
@@ -167,8 +159,6 @@ Example response:
Removes a user from a group or project.
-Returns `200` if the request succeeds.
-
```
DELETE /groups/:id/members/:user_id
DELETE /projects/:id/members/:user_id
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index f4167403c2c..662cc9da733 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -11,6 +11,7 @@ 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
```
Parameters:
@@ -270,17 +271,18 @@ Creates a new merge request.
POST /projects/:id/merge_requests
```
-Parameters:
-
-- `id` (required) - The ID of a project
-- `source_branch` (required) - The source branch
-- `target_branch` (required) - The target branch
-- `assignee_id` (optional) - Assignee user ID
-- `title` (required) - Title of MR
-- `description` (optional) - Description of MR
-- `target_project_id` (optional) - The target project (numeric id)
-- `labels` (optional) - Labels for MR as a comma-separated list
-- `milestone_id` (optional) - Milestone ID
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | string | yes | The ID of a project |
+| `source_branch` | string | yes | The source branch |
+| `target_branch` | string | yes | The target branch |
+| `title` | string | yes | 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) |
+| `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 |
```json
{
@@ -337,9 +339,6 @@ Parameters:
}
```
-If the operation is successful, 200 and the newly created merge request is returned.
-If an error occurs, an error number and a message explaining the reason is returned.
-
## Update MR
Updates an existing merge request. You can change the target branch, title, or even close the MR.
@@ -348,17 +347,19 @@ Updates an existing merge request. You can change the target branch, title, or e
PUT /projects/:id/merge_requests/:merge_request_id
```
-Parameters:
-
-- `id` (required) - The ID of a project
-- `merge_request_id` (required) - ID of MR
-- `target_branch` - The target branch
-- `assignee_id` - Assignee user ID
-- `title` - Title of MR
-- `description` - Description of MR
-- `state_event` - New state (close|reopen|merge)
-- `labels` (optional) - Labels for MR as a comma-separated list
-- `milestone_id` (optional) - Milestone ID
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `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 |
+| `assignee_id` | integer | no | Assignee user ID |
+| `description` | string | no | Description of MR |
+| `target_project_id` | integer | no | The target project (numeric id) |
+| `labels` | string | no | Labels for MR as a comma-separated list |
+| `milestone_id` | integer | no | The ID of a milestone |
+| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
```json
{
@@ -414,14 +415,9 @@ Parameters:
}
```
-If the operation is successful, 200 and the updated merge request is returned.
-If an error occurs, an error number and a message explaining the reason is returned.
-
## Delete a merge request
Only for admins and project owners. Soft deletes the merge request in question.
-If the operation is successful, a status code `200` is returned. In case you cannot
-destroy this merge request, or it is not present, code `404` is given.
```
DELETE /projects/:id/merge_requests/:merge_request_id
@@ -433,22 +429,21 @@ DELETE /projects/:id/merge_requests/:merge_request_id
| `merge_request_id` | integer | yes | The ID of a project's merge request |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_request/85
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_requests/85
```
## Accept MR
Merge changes submitted with MR using this API.
-If the merge succeeds you'll get a `200 OK`.
-If it has some conflicts and can not be merged - you'll get a 405 and the error message 'Branch cannot be merged'
+If it has some conflicts and can not be merged - you'll get a `405` and the error message 'Branch cannot be merged'
-If merge request is already merged or closed - you'll get a 406 and the error message 'Method Not Allowed'
+If merge request is already merged or closed - you'll get a `406` and the error message 'Method Not Allowed'
-If the `sha` parameter is passed and does not match the HEAD of the source - you'll get a 409 and the error message 'SHA does not match HEAD of source branch'
+If the `sha` parameter is passed and does not match the HEAD of the source - you'll get a `409` and the error message 'SHA does not match HEAD of source branch'
-If you don't have permissions to accept this merge request - you'll get a 401
+If you don't have permissions to accept this merge request - you'll get a `401`
```
PUT /projects/:id/merge_requests/:merge_request_id/merge
@@ -518,15 +513,13 @@ Parameters:
}
```
-## Cancel Merge When Build Succeeds
-
-If successful you'll get `200 OK`.
+## Cancel Merge When Pipeline Succeeds
-If you don't have permissions to accept this merge request - you'll get a 401
+If you don't have permissions to accept this merge request - you'll get a `401`
-If the merge request is already merged or closed - you get 405 and error message 'Method Not Allowed'
+If the merge request is already merged or closed - you get `405` and error message 'Method Not Allowed'
-In case the merge request is not set to be merged when the build succeeds, you'll also get a 406 error.
+In case the merge request is not set to be merged when the build succeeds, you'll also get a `406` error.
```
PUT /projects/:id/merge_requests/:merge_request_id/cancel_merge_when_build_succeeds
```
@@ -670,11 +663,8 @@ Example response when an external issue tracker (e.g. JIRA) is used:
## Subscribe to a merge request
-Subscribes the authenticated user to a merge request to receive notification. If
-the operation is successful, status code `201` together with the updated merge
-request is returned. If the user is already subscribed to the merge request, the
-status code `304` is returned. If the project or merge request is not found,
-status code `404` is returned.
+Subscribes the authenticated user to a merge request to receive notification. If the user is already subscribed to the merge request, the
+status code `304` is returned.
```
POST /projects/:id/merge_requests/:merge_request_id/subscription
@@ -747,10 +737,8 @@ Example response:
## Unsubscribe from a merge request
Unsubscribes the authenticated user from a merge request to not receive
-notifications from that merge request. If the operation is successful, status
-code `200` together with the updated merge request is returned. If the user is
-not subscribed to the merge request, the status code `304` is returned. If the
-project or merge request is not found, status code `404` is returned.
+notifications from that merge request. If the user is
+not subscribed to the merge request, the status code `304` is returned.
```
DELETE /projects/:id/merge_requests/:merge_request_id/subscription
@@ -822,9 +810,8 @@ Example response:
## Create a todo
-Manually creates a todo for the current user on a merge request. If the
-request is successful, status code `200` together with the created todo is
-returned. If there already exists a todo for the user on that merge request,
+Manually creates a todo for the current user on a merge request.
+If there already exists a todo for the user on that merge request,
status code `304` is returned.
```
diff --git a/doc/api/milestones.md b/doc/api/milestones.md
index ae7d22a4be5..12497acff98 100644
--- a/doc/api/milestones.md
+++ b/doc/api/milestones.md
@@ -7,6 +7,7 @@ Returns a list of project milestones.
```
GET /projects/:id/milestones
GET /projects/:id/milestones?iid=42
+GET /projects/:id/milestones?iid[]=42&iid[]=43
GET /projects/:id/milestones?state=active
GET /projects/:id/milestones?state=closed
```
@@ -16,7 +17,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project |
-| `iid` | integer | optional | Return only the milestone having the given `iid` |
+| `iid` | Array[integer] | optional | Return only the milestone having the given `iid` |
| `state` | string | optional | Return only `active` or `closed` milestones` |
```bash
@@ -34,6 +35,7 @@ Example Response:
"title": "10.0",
"description": "Version",
"due_date": "2013-11-29",
+ "start_date": "2013-11-10",
"state": "active",
"updated_at": "2013-10-02T09:24:18Z",
"created_at": "2013-10-02T09:24:18Z"
@@ -69,6 +71,7 @@ Parameters:
- `title` (required) - The title of an milestone
- `description` (optional) - The description of the milestone
- `due_date` (optional) - The due date of the milestone
+- `start_date` (optional) - The start date of the milestone
## Edit milestone
@@ -85,6 +88,7 @@ Parameters:
- `title` (optional) - The title of a milestone
- `description` (optional) - The description of a milestone
- `due_date` (optional) - The due date of the milestone
+- `start_date` (optional) - The start date of the milestone
- `state_event` (optional) - The state event of the milestone (close|activate)
## Get all issues assigned to a single milestone
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 58d40eecf3e..214dfa4068d 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -21,7 +21,7 @@ Parameters:
[
{
"id": 302,
- "body": "Status changed to closed",
+ "body": "closed",
"attachment": null,
"author": {
"id": 1,
@@ -109,8 +109,7 @@ Parameters:
### Delete an issue note
-Deletes an existing note of an issue. On success, this API method returns 200
-and the deleted note. If the note does not exist, the API returns 404.
+Deletes an existing note of an issue.
```
DELETE /projects/:id/issues/:issue_id/notes/:note_id
@@ -234,8 +233,7 @@ Parameters:
### Delete a snippet note
-Deletes an existing note of a snippet. On success, this API method returns 200
-and the deleted note. If the note does not exist, the API returns 404.
+Deletes an existing note of a snippet.
```
DELETE /projects/:id/snippets/:snippet_id/notes/:note_id
@@ -364,8 +362,7 @@ Parameters:
### Delete a merge request note
-Deletes an existing note of a merge request. On success, this API method returns
-200 and the deleted note. If the note does not exist, the API returns 404.
+Deletes an existing note of a merge request.
```
DELETE /projects/:id/merge_requests/:merge_request_id/notes/:note_id
diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md
index ff6c9e4931c..aea1c12a392 100644
--- a/doc/api/notification_settings.md
+++ b/doc/api/notification_settings.md
@@ -4,7 +4,7 @@
**Valid notification levels**
-The notification levels are defined in the `NotificationSetting::level` model enumeration. Currently, these levels are recognized:
+The notification levels are defined in the `NotificationSetting.level` model enumeration. Currently, these levels are recognized:
```
disabled
@@ -28,6 +28,8 @@ reopen_merge_request
close_merge_request
reassign_merge_request
merge_merge_request
+failed_pipeline
+success_pipeline
```
## Global notification settings
@@ -77,6 +79,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| `close_merge_request` | boolean | no | Enable/disable this notification |
| `reassign_merge_request` | boolean | no | Enable/disable this notification |
| `merge_merge_request` | boolean | no | Enable/disable this notification |
+| `failed_pipeline` | boolean | no | Enable/disable this notification |
+| `success_pipeline` | boolean | no | Enable/disable this notification |
Example response:
@@ -141,6 +145,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| `close_merge_request` | boolean | no | Enable/disable this notification |
| `reassign_merge_request` | boolean | no | Enable/disable this notification |
| `merge_merge_request` | boolean | no | Enable/disable this notification |
+| `failed_pipeline` | boolean | no | Enable/disable this notification |
+| `success_pipeline` | boolean | no | Enable/disable this notification |
Example responses:
@@ -161,7 +167,9 @@ Example responses:
"reopen_merge_request": false,
"close_merge_request": false,
"reassign_merge_request": false,
- "merge_merge_request": false
+ "merge_merge_request": false,
+ "failed_pipeline": false,
+ "success_pipeline": false
}
}
```
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index a29b3eb6f44..82351ae688f 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -41,7 +41,8 @@ Example of response
"started_at": null,
"finished_at": null,
"committed_at": null,
- "duration": null
+ "duration": null,
+ "coverage": "30.0"
},
{
"id": 48,
@@ -64,7 +65,8 @@ Example of response
"started_at": null,
"finished_at": null,
"committed_at": null,
- "duration": null
+ "duration": null,
+ "coverage": null
}
]
```
@@ -110,7 +112,54 @@ Example of response
"started_at": null,
"finished_at": "2016-08-11T11:32:35.145Z",
"committed_at": null,
- "duration": null
+ "duration": null,
+ "coverage": "30.0"
+}
+```
+
+## Create a new pipeline
+
+> [Introduced][ce-7209] in GitLab 8.14
+
+```
+POST /projects/:id/pipeline
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `ref` | string | yes | Reference to commit |
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipeline?ref=master"
+```
+
+Example of response
+
+```json
+{
+ "id": 61,
+ "sha": "384c444e840a515b23f21915ee5766b87068a70d",
+ "ref": "master",
+ "status": "pending",
+ "before_sha": "0000000000000000000000000000000000000000",
+ "tag": false,
+ "yaml_errors": null,
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2016-11-04T09:36:13.747Z",
+ "updated_at": "2016-11-04T09:36:13.977Z",
+ "started_at": null,
+ "finished_at": null,
+ "committed_at": null,
+ "duration": null,
+ "coverage": null
}
```
@@ -155,7 +204,8 @@ Response:
"started_at": null,
"finished_at": "2016-08-11T11:32:35.145Z",
"committed_at": null,
- "duration": null
+ "duration": null,
+ "coverage": null
}
```
@@ -200,8 +250,10 @@ Response:
"started_at": null,
"finished_at": "2016-08-11T11:32:35.145Z",
"committed_at": null,
- "duration": null
+ "duration": null,
+ "coverage": null
}
```
[ce-5837]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5837
+[ce-7209]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7209
diff --git a/doc/api/projects.md b/doc/api/projects.md
index bbb3bfb4995..edffad555a5 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -347,7 +347,8 @@ Parameters:
### Get single project
Get a specific project, identified by project ID or NAMESPACE/PROJECT_NAME, which is owned by the authenticated user.
-If using namespaced projects call make sure that the NAMESPACE/PROJECT_NAME is URL-encoded, eg. `/api/v3/projects/diaspora%2Fdiaspora` (where `/` is represented by `%2F`).
+If using namespaced projects call make sure that the NAMESPACE/PROJECT_NAME is URL-encoded, eg. `/api/v3/projects/diaspora%2Fdiaspora` (where `/` is represented by `%2F`). This endpoint can be accessed without authentication if
+the project is publicly accessible.
```
GET /projects/:id
@@ -436,10 +437,47 @@ Parameters:
}
```
+## Get project users
+
+Get the users list of a project.
+
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `search` | string | no | Search for specific users |
+
+```
+GET /projects/:id/users
+```
+
+```json
+[
+ {
+ "id": 1,
+ "username": "john_smith",
+ "name": "John Smith",
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
+ "web_url": "http://localhost:3000/john_smith"
+ },
+ {
+ "id": 2,
+ "username": "jack_smith",
+ "name": "Jack Smith",
+ "state": "blocked",
+ "avatar_url": "http://gravatar.com/../e32131cd8.jpeg",
+ "web_url": "http://localhost:3000/jack_smith"
+ }
+]
+```
+
### Get project events
-Get the events for the specified project.
-Sorted from newest to oldest
+Get the events for the specified project sorted from newest to oldest. This
+endpoint can be accessed without authentication if the project is publicly
+accessible.
```
GET /projects/:id/events
@@ -624,6 +662,7 @@ Parameters:
| `user_id` | integer | yes | The user ID of the project owner |
| `name` | string | yes | The name of the new project |
| `path` | string | no | Custom repository name for new project. By default generated based on name |
+| `default_branch` | string | no | `master` by default |
| `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) |
| `description` | string | no | Short project description |
| `issues_enabled` | boolean | no | Enable issues for this project |
@@ -644,7 +683,7 @@ Parameters:
### Edit project
-Updates an existing project
+Updates an existing project.
```
PUT /projects/:id
@@ -657,6 +696,7 @@ Parameters:
| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
| `name` | string | yes | The name of the project |
| `path` | string | no | Custom repository name for the project. By default generated based on name |
+| `default_branch` | string | no | `master` by default |
| `description` | string | no | Short project description |
| `issues_enabled` | boolean | no | Enable issues for this project |
| `merge_requests_enabled` | boolean | no | Enable merge requests for this project |
@@ -674,9 +714,6 @@ Parameters:
| `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access |
-On success, method returns 200 with the updated project. If parameters are
-invalid, 400 is returned.
-
### Fork project
Forks a project into the user namespace of the authenticated user or the one provided.
@@ -694,8 +731,7 @@ Parameters:
### Star a project
-Stars a given project. Returns status code `201` and the project on success and
-`304` if the project is already starred.
+Stars a given project. Returns status code `304` if the project is already starred.
```
POST /projects/:id/star
@@ -765,8 +801,7 @@ Example response:
### Unstar a project
-Unstars a given project. Returns status code `200` and the project on success
-and `304` if the project is not starred.
+Unstars a given project. Returns status code `304` if the project is not starred.
```
DELETE /projects/:id/star
@@ -837,10 +872,6 @@ Example response:
Archives the project if the user is either admin or the project owner of this project. This action is
idempotent, thus archiving an already archived project will not change the project.
-Status code 201 with the project as body is given when successful, in case the user doesn't
-have the proper access rights, code 403 is returned. Status 404 is returned if the project
-doesn't exist, or is hidden to the user.
-
```
POST /projects/:id/archive
```
@@ -850,7 +881,7 @@ POST /projects/:id/archive
| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/archive"
```
Example response:
@@ -926,10 +957,6 @@ Example response:
Unarchives the project if the user is either admin or the project owner of this project. This action is
idempotent, thus unarchiving an non-archived project will not change the project.
-Status code 201 with the project as body is given when successful, in case the user doesn't
-have the proper access rights, code 403 is returned. Status 404 is returned if the project
-doesn't exist, or is hidden to the user.
-
```
POST /projects/:id/unarchive
```
@@ -939,7 +966,7 @@ POST /projects/:id/unarchive
| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/unarchive"
```
Example response:
@@ -1074,6 +1101,25 @@ Parameters:
| `group_access` | integer | yes | The permissions level to grant the group |
| `expires_at` | string | no | Share expiration date in ISO 8601 format: 2016-09-26 |
+### Delete a shared project link within a group
+
+Unshare the project from the group. Returns `204` and no content on success.
+
+```
+DELETE /projects/:id/share/:group_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `group_id` | integer | yes | The ID of the group |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/share/17
+```
+
## Hooks
Also called Project Hooks and Webhooks.
@@ -1335,7 +1381,9 @@ Parameter:
## Search for projects by name
-Search for projects by name which are accessible to the authenticated user.
+Search for projects by name which are accessible to the authenticated user. This
+endpoint can be accessed without authentication if the project is publicly
+accessible.
```
GET /projects/search/:query
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index b6cca5d4e2a..bcf8b955044 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -13,44 +13,58 @@ Parameters:
- `id` (required) - The ID of a project
- `path` (optional) - The path inside repository. Used to get contend of subdirectories
- `ref_name` (optional) - The name of a repository branch or tag or if not given the default branch
+- `recursive` (optional) - Boolean value used to get a recursive tree (false by default)
```json
[
{
- "name": "assets",
+ "id": "a1e8f8d745cc87e3a9248358d9352bb7f9a0aeba",
+ "name": "html",
"type": "tree",
- "mode": "040000",
- "id": "6229c43a7e16fcc7e95f923f8ddadb8281d9c6c6"
+ "path": "files/html",
+ "mode": "040000"
},
{
- "name": "contexts",
+ "id": "4535904260b1082e14f867f7a24fd8c21495bde3",
+ "name": "images",
"type": "tree",
- "mode": "040000",
- "id": "faf1cdf33feadc7973118ca42d35f1e62977e91f"
+ "path": "files/images",
+ "mode": "040000"
},
{
- "name": "controllers",
+ "id": "31405c5ddef582c5a9b7a85230413ff90e2fe720",
+ "name": "js",
"type": "tree",
- "mode": "040000",
- "id": "95633e8d258bf3dfba3a5268fb8440d263218d74"
+ "path": "files/js",
+ "mode": "040000"
},
{
- "name": "Rakefile",
- "type": "blob",
- "mode": "100644",
- "id": "35b2f05cbb4566b71b34554cf184a9d0bd9d46d6"
+ "id": "cc71111cfad871212dc99572599a568bfe1e7e00",
+ "name": "lfs",
+ "type": "tree",
+ "path": "files/lfs",
+ "mode": "040000"
},
{
- "name": "VERSION",
- "type": "blob",
- "mode": "100644",
- "id": "803e4a4f3727286c3093c63870c2b6524d30ec4f"
+ "id": "fd581c619bf59cfdfa9c8282377bb09c2f897520",
+ "name": "markdown",
+ "type": "tree",
+ "path": "files/markdown",
+ "mode": "040000"
+ },
+ {
+ "id": "23ea4d11a4bdd960ee5320c5cb65b5b3fdbc60db",
+ "name": "ruby",
+ "type": "tree",
+ "path": "files/ruby",
+ "mode": "040000"
},
{
- "name": "config.ru",
+ "id": "7d70e02340bac451f281cecf0a980907974bd8be",
+ "name": "whitespace",
"type": "blob",
- "mode": "100644",
- "id": "dfd2d862237323aa599be31b473d70a8a817943b"
+ "path": "files/whitespace",
+ "mode": "100644"
}
]
```
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index 1bc6a24e914..b8c9eb2c9a8 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -60,7 +60,7 @@ Parameters:
- `file_path` (required) - Full path to new file. Ex. lib/class.rb
- `branch_name` (required) - The name of branch
-- `encoding` (optional) - 'text' or 'base64'. Text is default.
+- `encoding` (optional) - Change encoding to 'base64'. Default is text.
- `author_email` (optional) - Specify the commit author's email address
- `author_name` (optional) - Specify the commit author's name
- `content` (required) - File content
@@ -89,7 +89,7 @@ Parameters:
- `file_path` (required) - Full path to file. Ex. lib/class.rb
- `branch_name` (required) - The name of branch
-- `encoding` (optional) - 'text' or 'base64'. Text is default.
+- `encoding` (optional) - Change encoding to 'base64'. Default is text.
- `author_email` (optional) - Specify the commit author's email address
- `author_name` (optional) - Specify the commit author's name
- `content` (required) - New file content
diff --git a/doc/api/services.md b/doc/api/services.md
index c7f537aceb6..1466b8189b0 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -139,6 +139,43 @@ Get Buildkite service settings for a project.
GET /projects/:id/services/buildkite
```
+## Build-Emails
+
+Get emails for GitLab CI builds.
+
+### Create/Edit Build-Emails service
+
+Set Build-Emails service for a project.
+
+```
+PUT /projects/:id/services/builds-email
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `recipients` | string | yes | Comma-separated list of recipient email addresses |
+| `add_pusher` | boolean | no | Add pusher to recipients list |
+| `notify_only_broken_builds` | boolean | no | Notify only broken builds |
+
+
+### Delete Build-Emails service
+
+Delete Build-Emails service for a project.
+
+```
+DELETE /projects/:id/services/builds-email
+```
+
+### Get Build-Emails service settings
+
+Get Build-Emails service settings for a project.
+
+```
+GET /projects/:id/services/builds-email
+```
+
## Campfire
Simple web-based real-time group chat
@@ -465,10 +502,10 @@ GET /projects/:id/services/jira
Set JIRA service for a project.
->**Note:**
-Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to
-easily navigate to the JIRA issue tracker. See the [integration doc][jira-doc]
-for details.
+>**Notes:**
+- Starting with GitLab 8.14, `api_url`, `issues_url`, `new_issue_url` and
+ `project_url` are replaced by `project_key`, `url`. If you are using an
+ older version, [follow this documentation][old-jira-api].
```
PUT /projects/:id/services/jira
@@ -476,15 +513,11 @@ PUT /projects/:id/services/jira
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `active` | boolean| no | Enable/disable the JIRA service. |
-| `project_url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project. It is of the form: `https://<jira_host_url>/issues/?jql=project=<jira_project>`. |
-| `issues_url` | string | yes | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. It is of the form: `https://<jira_host_url>/browse/:id`. Leave `:id` as-is, it gets replaced by GitLab at runtime.|
-| `new_issue_url` | string | yes | This is the URL to create a new issue in JIRA for the project linked to this GitLab project, and it is of the form: `https://<jira_host_url>/secure/CreateIssue.jspa` |
-| `api_url` | string | yes | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https://<jira_host_url>/rest/api/2`. |
-| `description` | string | no | A name for the issue tracker. |
+| `url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project, e.g., `https://jira.example.com`. |
+| `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. |
| `username` | string | no | The username of the user created to be used with GitLab/JIRA. |
| `password` | string | no | The password of the user created to be used with GitLab/JIRA. |
-| `jira_issue_transition_id` | string | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. |
+| `jira_issue_transition_id` | integer | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. |
### Delete JIRA service
@@ -494,6 +527,78 @@ Remove all previously JIRA settings from a project.
DELETE /projects/:id/services/jira
```
+## Mattermost Slash Commands
+
+Ability to receive slash commands from a Mattermost chat instance.
+
+### Create/Edit Mattermost Slash Command service
+
+Set Mattermost Slash Command for a project.
+
+```
+PUT /projects/:id/services/mattermost-slash-commands
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `token` | string | yes | The Mattermost token |
+
+
+### Delete Mattermost Slash Command service
+
+Delete Mattermost Slash Command service for a project.
+
+```
+DELETE /projects/:id/services/mattermost-slash-commands
+```
+
+### Get Mattermost Slash Command service settings
+
+Get Mattermost Slash Command service settings for a project.
+
+```
+GET /projects/:id/services/mattermost-slash-commands
+```
+
+## Pipeline-Emails
+
+Get emails for GitLab CI pipelines.
+
+### Create/Edit Pipeline-Emails service
+
+Set Pipeline-Emails service for a project.
+
+```
+PUT /projects/:id/services/pipelines-email
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `recipients` | string | yes | Comma-separated list of recipient email addresses |
+| `add_pusher` | boolean | no | Add pusher to recipients list |
+| `notify_only_broken_builds` | boolean | no | Notify only broken pipelines |
+
+
+### Delete Pipeline-Emails service
+
+Delete Pipeline-Emails service for a project.
+
+```
+DELETE /projects/:id/services/pipelines-email
+```
+
+### Get Pipeline-Emails service settings
+
+Get Pipeline-Emails service settings for a project.
+
+```
+GET /projects/:id/services/pipelines-email
+```
+
## PivotalTracker
Project Management Software (Source Commits Endpoint)
@@ -598,9 +703,9 @@ Get Redmine service settings for a project.
GET /projects/:id/services/redmine
```
-## Slack
+## Slack notifications
-A team communication tool for the 21st century
+Receive event notifications in Slack
### Create/Edit Slack service
@@ -632,6 +737,40 @@ Get Slack service settings for a project.
GET /projects/:id/services/slack
```
+## Mattermost notifications
+
+Receive event notifications in Mattermost
+
+### Create/Edit Mattermost notifications service
+
+Set Mattermost service for a project.
+
+```
+PUT /projects/:id/services/mattermost
+```
+
+Parameters:
+
+- `webhook` (**required**) - https://mattermost.example/hooks/1298aff...
+- `username` (optional) - username
+- `channel` (optional) - #channel
+
+### Delete Mattermost notifications service
+
+Delete Mattermost Notifications service for a project.
+
+```
+DELETE /projects/:id/services/mattermost
+```
+
+### Get Mattermost notifications service settings
+
+Get Mattermost notifications service settings for a project.
+
+```
+GET /projects/:id/services/mattermost
+```
+
## JetBrains TeamCity CI
A continuous integration and build server
@@ -670,3 +809,4 @@ GET /projects/:id/services/teamcity
```
[jira-doc]: ../project_services/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/snippets.md b/doc/api/snippets.md
new file mode 100644
index 00000000000..5a5dc162ffe
--- /dev/null
+++ b/doc/api/snippets.md
@@ -0,0 +1,232 @@
+# Snippets
+
+> [Introduced][ce-6373] in GitLab 8.15.
+
+### Snippet visibility level
+
+Snippets in GitLab can be either private, internal, or public.
+You can set it with the `visibility_level` field in the snippet.
+
+Constants for snippet visibility levels are:
+
+| Visibility | Visibility level | Description |
+| ---------- | ---------------- | ----------- |
+| Private | `0` | The snippet is visible only to the snippet creator |
+| Internal | `10` | The snippet is visible for any logged in user |
+| Public | `20` | The snippet can be accessed without any authentication |
+
+## List snippets
+
+Get a list of current user's snippets.
+
+```
+GET /snippets
+```
+
+## Single snippet
+
+Get a single snippet.
+
+```
+GET /snippets/:id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | Integer | yes | The ID of a snippet |
+
+``` bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/1
+```
+
+Example response:
+
+``` json
+{
+ "id": 1,
+ "title": "test",
+ "file_name": "add.rb",
+ "author": {
+ "id": 1,
+ "username": "john_smith",
+ "email": "john@example.com",
+ "name": "John Smith",
+ "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/snippets/1",
+}
+```
+
+## Create new snippet
+
+Creates a new snippet. The user must have permission to create new snippets.
+
+```
+POST /snippets
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `title` | String | yes | The title of a snippet |
+| `file_name` | String | yes | The name of a snippet file |
+| `content` | String | yes | The content of a snippet |
+| `visibility_level` | Integer | yes | The snippet's visibility |
+
+
+``` bash
+curl --request POST --data '{"title": "This is a snippet", "content": "Hello world", "file_name": "test.txt", "visibility_level": 10 }' --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets
+```
+
+Example response:
+
+``` json
+{
+ "id": 1,
+ "title": "This is a snippet",
+ "file_name": "test.txt",
+ "author": {
+ "id": 1,
+ "username": "john_smith",
+ "email": "john@example.com",
+ "name": "John Smith",
+ "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/snippets/1",
+}
+```
+
+## Update snippet
+
+Updates an existing snippet. The user must have permission to change an existing snippet.
+
+```
+PUT /snippets/:id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | Integer | yes | The ID of a snippet |
+| `title` | String | no | The title of a snippet |
+| `file_name` | String | no | The name of a snippet file |
+| `content` | String | no | The content of a snippet |
+| `visibility_level` | Integer | no | The snippet's visibility |
+
+
+``` bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data '{"title": "foo", "content": "bar"}' https://gitlab.example.com/api/v3/snippets/1
+```
+
+Example response:
+
+``` json
+{
+ "id": 1,
+ "title": "test",
+ "file_name": "add.rb",
+ "author": {
+ "id": 1,
+ "username": "john_smith",
+ "email": "john@example.com",
+ "name": "John Smith",
+ "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/snippets/1",
+}
+```
+
+## Delete snippet
+
+Deletes an existing snippet.
+
+```
+DELETE /snippets/:id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | Integer | yes | The ID of a snippet |
+
+
+```
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/snippets/1"
+```
+
+upon successful delete a `204 No content` HTTP code shall be expected, with no data,
+but if the snippet is non-existent, a `404 Not Found` will be returned.
+
+## Explore all public snippets
+
+```
+GET /snippets/public
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `per_page` | Integer | no | number of snippets to return per page |
+| `page` | Integer | no | the page to retrieve |
+
+``` bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/public?per_page=2&page=1
+```
+
+Example response:
+
+``` json
+[
+ {
+ "author": {
+ "avatar_url": "http://www.gravatar.com/avatar/edaf55a9e363ea263e3b981d09e0f7f7?s=80&d=identicon",
+ "id": 12,
+ "name": "Libby Rolfson",
+ "state": "active",
+ "username": "elton_wehner",
+ "web_url": "http://localhost:3000/elton_wehner"
+ },
+ "created_at": "2016-11-25T16:53:34.504Z",
+ "file_name": "oconnerrice.rb",
+ "id": 49,
+ "raw_url": "http://localhost:3000/snippets/49/raw",
+ "title": "Ratione cupiditate et laborum temporibus.",
+ "updated_at": "2016-11-25T16:53:34.504Z",
+ "web_url": "http://localhost:3000/snippets/49"
+ },
+ {
+ "author": {
+ "avatar_url": "http://www.gravatar.com/avatar/36583b28626de71061e6e5a77972c3bd?s=80&d=identicon",
+ "id": 16,
+ "name": "Llewellyn Flatley",
+ "state": "active",
+ "username": "adaline",
+ "web_url": "http://localhost:3000/adaline"
+ },
+ "created_at": "2016-11-25T16:53:34.479Z",
+ "file_name": "muellershields.rb",
+ "id": 48,
+ "raw_url": "http://localhost:3000/snippets/48/raw",
+ "title": "Minus similique nesciunt vel fugiat qui ullam sunt.",
+ "updated_at": "2016-11-25T16:53:34.479Z",
+ "web_url": "http://localhost:3000/snippets/48"
+ }
+]
+```
+
diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md
index efd23d514bc..3fb8b73be6d 100644
--- a/doc/api/system_hooks.md
+++ b/doc/api/system_hooks.md
@@ -108,8 +108,7 @@ Example response:
## Delete system hook
-Deletes a system hook. It returns `200 OK` if the hooks is deleted and
-`404 Not Found` if the hook is not found.
+Deletes a system hook.
---
diff --git a/doc/api/tags.md b/doc/api/tags.md
index 398b080e3f6..7f78ffc2390 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -2,7 +2,9 @@
## List project repository tags
-Get a list of repository tags from a project, sorted by name in reverse alphabetical order.
+Get a list of repository tags from a project, sorted by name in reverse
+alphabetical order. This endpoint can be accessed without authentication if the
+repository is publicly accessible.
```
GET /projects/:id/repository/tags
@@ -40,9 +42,8 @@ Parameters:
## Get a single repository tag
-Get a specific repository tag determined by its name. It returns `200` together
-with the tag information if the tag exists. It returns `404` if the tag does not
-exist.
+Get a specific repository tag determined by its name. This endpoint can be
+accessed without authentication if the repository is publicly accessible.
```
GET /projects/:id/repository/tags/:tag_name
@@ -124,14 +125,12 @@ Parameters:
The message will be `nil` when creating a lightweight tag otherwise
it will contain the annotation.
-It returns 201 if the operation succeed. In case of an error,
-405 with an explaining error message is returned.
+In case of an error,
+status code `405` with an explaining error message is returned.
## Delete a tag
-Deletes a tag of a repository with given name. On success, this API method
-returns 200 with the name of the deleted tag. If the tag does not exist, the
-API returns 404.
+Deletes a tag of a repository with given name.
```
DELETE /projects/:id/repository/tags/:tag_name
@@ -150,9 +149,8 @@ Parameters:
## Create a new release
-Add release notes to the existing git tag. It returns 201 if the release is
-created successfully. If the tag does not exist, 404 is returned. If there
-already exists a release for the given tag, 409 is returned.
+Add release notes to the existing git tag. If there
+already exists a release for the given tag, status code `409` is returned.
```
POST /projects/:id/repository/tags/:tag_name/release
@@ -173,9 +171,7 @@ Parameters:
## Update a release
-Updates the release notes of a given release. It returns 200 if the release is
-successfully updated. If the tag or the release does not exist, it returns 404
-with a proper error message.
+Updates the release notes of a given release.
```
PUT /projects/:id/repository/tags/:tag_name/release
diff --git a/doc/api/users.md b/doc/api/users.md
index a50ba5432fe..28b6c7bd491 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -33,6 +33,18 @@ GET /users
]
```
+In addition, you can filter users based on states eg. `blocked`, `active`
+This works only to filter users who are `blocked` or `active`.
+It does not support `active=false` or `blocked=false`.
+
+```
+GET /users?active=true
+```
+
+```
+GET /users?blocked=true
+```
+
### For admins
```
@@ -120,6 +132,8 @@ For example:
GET /users?username=jack_smith
```
+You can search for users who are external with: `/users?external=true`
+
## Single user
Get a single user.
@@ -257,8 +271,8 @@ Parameters:
- `can_create_group` (optional) - User can create groups - true or false
- `external` (optional) - Flags the user as external - true or false(default)
-Note, at the moment this method does only return a 404 error,
-even in cases where a 409 (Conflict) would be more appropriate,
+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.
## User deletion
@@ -277,7 +291,9 @@ Parameters:
- `id` (required) - The ID of the user
-## Current user
+## User
+
+### For normal users
Gets currently authenticated user.
@@ -321,6 +337,53 @@ GET /user
}
```
+### For admins
+
+Parameters:
+
+- `sudo` (required) - the ID of a user
+
+```
+GET /user
+```
+
+```json
+{
+ "id": 1,
+ "username": "john_smith",
+ "email": "john@example.com",
+ "name": "John Smith",
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
+ "web_url": "http://localhost:3000/john_smith",
+ "created_at": "2012-05-23T08:00:58Z",
+ "is_admin": false,
+ "bio": null,
+ "location": null,
+ "skype": "",
+ "linkedin": "",
+ "twitter": "",
+ "website_url": "",
+ "organization": "",
+ "last_sign_in_at": "2012-06-01T11:41:01Z",
+ "confirmed_at": "2012-05-23T09:05:22Z",
+ "theme_id": 1,
+ "color_scheme_id": 2,
+ "projects_limit": 100,
+ "current_sign_in_at": "2012-06-02T06:36:55Z",
+ "identities": [
+ {"provider": "github", "extern_uid": "2435223452345"},
+ {"provider": "bitbucket", "extern_uid": "john_smith"},
+ {"provider": "google_oauth2", "extern_uid": "8776128412476123468721346"}
+ ],
+ "can_create_group": true,
+ "can_create_project": true,
+ "two_factor_enabled": true,
+ "external": false,
+ "private_token": "dd34asd13as"
+}
+```
+
## List SSH keys
Get a list of currently authenticated user's SSH keys.
@@ -355,24 +418,24 @@ Parameters:
Get a list of a specified user's SSH keys. Available only for admin
```
-GET /users/:uid/keys
+GET /users/:id/keys
```
Parameters:
-- `uid` (required) - id of specified user
+- `id` (required) - id of specified user
## Single SSH key
Get a single key.
```
-GET /user/keys/:id
+GET /user/keys/:key_id
```
Parameters:
-- `id` (required) - The ID of an SSH key
+- `key_id` (required) - The ID of an SSH key
```json
{
@@ -435,8 +498,6 @@ Parameters:
- `title` (required) - new SSH Key's title
- `key` (required) - new SSH key
-Will return created key with status `201 Created` on success, or `404 Not found` on fail.
-
## Delete SSH key for current user
Deletes key owned by currently authenticated user.
@@ -444,25 +505,25 @@ This is an idempotent function and calling it on a key that is already deleted
or not available results in `200 OK`.
```
-DELETE /user/keys/:id
+DELETE /user/keys/:key_id
```
Parameters:
-- `id` (required) - SSH key ID
+- `key_id` (required) - SSH key ID
## Delete SSH key for given user
Deletes key owned by a specified user. Available only for admin.
```
-DELETE /users/:uid/keys/:id
+DELETE /users/:id/keys/:key_id
```
Parameters:
-- `uid` (required) - id of specified user
-- `id` (required) - SSH key ID
+- `id` (required) - id of specified user
+- `key_id` (required) - SSH key ID
Will return `200 OK` on success, or `404 Not found` if either user or key cannot be found.
@@ -496,24 +557,24 @@ Parameters:
Get a list of a specified user's emails. Available only for admin
```
-GET /users/:uid/emails
+GET /users/:id/emails
```
Parameters:
-- `uid` (required) - id of specified user
+- `id` (required) - id of specified user
## Single email
Get a single email.
```
-GET /user/emails/:id
+GET /user/emails/:email_id
```
Parameters:
-- `id` (required) - email ID
+- `email_id` (required) - email ID
```json
{
@@ -567,8 +628,6 @@ Parameters:
- `id` (required) - id of specified user
- `email` (required) - email address
-Will return created email with status `201 Created` on success, or `404 Not found` on fail.
-
## Delete email for current user
Deletes email owned by currently authenticated user.
@@ -576,25 +635,25 @@ This is an idempotent function and calling it on a email that is already deleted
or not available results in `200 OK`.
```
-DELETE /user/emails/:id
+DELETE /user/emails/:email_id
```
Parameters:
-- `id` (required) - email ID
+- `email_id` (required) - email ID
## Delete email for given user
Deletes email owned by a specified user. Available only for admin.
```
-DELETE /users/:uid/emails/:id
+DELETE /users/:id/emails/:email_id
```
Parameters:
-- `uid` (required) - id of specified user
-- `id` (required) - email ID
+- `id` (required) - id of specified user
+- `email_id` (required) - email ID
Will return `200 OK` on success, or `404 Not found` if either user or email cannot be found.
@@ -603,12 +662,12 @@ Will return `200 OK` on success, or `404 Not found` if either user or email cann
Blocks the specified user. Available only for admin.
```
-PUT /users/:uid/block
+PUT /users/:id/block
```
Parameters:
-- `uid` (required) - id of specified user
+- `id` (required) - id of specified user
Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
`403 Forbidden` when trying to block an already blocked user by LDAP synchronization.
@@ -618,12 +677,12 @@ Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
Unblocks the specified user. Available only for admin.
```
-PUT /users/:uid/unblock
+PUT /users/:id/unblock
```
Parameters:
-- `uid` (required) - id of specified user
+- `id` (required) - id of specified user
Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
`403 Forbidden` when trying to unblock a user blocked by LDAP synchronization.
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 6b90940c047..73bd2516d46 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -1,6 +1,6 @@
-## GitLab CI Documentation
+# GitLab CI Documentation
-### CI User documentation
+## CI User documentation
- [Get started with GitLab CI](quick_start/README.md)
- [CI examples for various languages](examples/README.md)
@@ -11,7 +11,8 @@
- [Configure a Runner, the application that runs your builds](runners/README.md)
- [Use Docker images with GitLab Runner](docker/using_docker_images.md)
- [Use CI to build Docker images](docker/using_docker_build.md)
-- [Use variables in your `.gitlab-ci.yml`](variables/README.md)
+- [CI Variables](variables/README.md) - Learn how to use variables defined in
+ your `.gitlab-ci.yml` or secured ones defined in your project's settings
- [Use SSH keys in your build environment](ssh_keys/README.md)
- [Trigger builds through the API](triggers/README.md)
- [Build artifacts](../user/project/builds/artifacts.md)
@@ -20,4 +21,11 @@
- [API](../api/ci/README.md)
- [CI services (linked docker containers)](services/README.md)
- [CI/CD pipelines settings](../user/project/pipelines/settings.md)
-- [**New CI build permissions model**](../user/project/new_ci_build_permissions_model.md) Read about what changed in GitLab 8.12 and how that affects your builds. There's a new way to access your Git submodules and LFS objects in builds.
+- [Review Apps](review_apps/index.md)
+- [Git submodules](git_submodules.md) Using Git submodules in your CI jobs
+
+## Breaking changes
+
+- [New CI build permissions model](../user/project/new_ci_build_permissions_model.md)
+ Read about what changed in GitLab 8.12 and how that affects your builds.
+ There's a new way to access your Git submodules and LFS objects in builds.
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 959741f7338..28141cced3b 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -44,7 +44,8 @@ GitLab Runner then executes build scripts as the `gitlab-runner` user.
2. Install Docker Engine on server.
- For more information how to install Docker Engine on different systems checkout the [Supported installations](https://docs.docker.com/engine/installation/).
+ For more information how to install Docker Engine on different systems
+ checkout the [Supported installations](https://docs.docker.com/engine/installation/).
3. Add `gitlab-runner` user to `docker` group:
@@ -122,11 +123,17 @@ In order to do that, follow the steps:
Insecure = false
```
-1. You can now use `docker` in the build script (note the inclusion of the `docker:dind` service):
+1. You can now use `docker` in the build script (note the inclusion of the
+ `docker:dind` service):
```yaml
image: docker:latest
+ # When using dind, it's wise to use the overlayfs driver for
+ # improved performance.
+ variables:
+ DOCKER_DRIVER: overlay
+
services:
- docker:dind
@@ -140,15 +147,21 @@ In order to do that, follow the steps:
- docker run my-docker-image /script/to/run/tests
```
-Docker-in-Docker works well, and is the recommended configuration, but it is not without its own challenges:
-* By enabling `--docker-privileged`, you are effectively disabling all of
-the security mechanisms of containers and exposing your host to privilege
-escalation which can lead to container breakout. For more information, check out the official Docker documentation on
-[Runtime privilege and Linux capabilities][docker-cap].
-* Using docker-in-docker, each build is in a clean environment without the past
-history. Concurrent builds work fine because every build gets it's own instance of docker engine so they won't conflict with each other. But this also means builds can be slower because there's no caching of layers.
-* By default, `docker:dind` uses `--storage-driver vfs` which is the slowest form
-offered.
+Docker-in-Docker works well, and is the recommended configuration, but it is
+not without its own challenges:
+
+- By enabling `--docker-privileged`, you are effectively disabling all of
+ the security mechanisms of containers and exposing your host to privilege
+ escalation which can lead to container breakout. For more information, check
+ out the official Docker documentation on
+ [Runtime privilege and Linux capabilities][docker-cap].
+- Using docker-in-docker, each build is in a clean environment without the past
+ history. Concurrent builds work fine because every build gets it's own
+ instance of Docker engine so they won't conflict with each other. But this
+ also means builds can be slower because there's no caching of layers.
+- By default, `docker:dind` uses `--storage-driver vfs` which is the slowest
+ form offered. To use a different driver, see
+ [Using the overlayfs driver](#using-the-overlayfs-driver).
An example project using this approach can be found here: https://gitlab.com/gitlab-examples/docker.
@@ -221,14 +234,52 @@ work as expected since volume mounting is done in the context of the host
machine, not the build container.
e.g. `docker run --rm -t -i -v $(pwd)/src:/home/app/src test-image:latest run_app_tests`
-## Using the GitLab Container Registry
+## Using the OverlayFS driver
-> **Note:**
-This feature requires GitLab 8.8 and GitLab Runner 1.2.
+By default, when using `docker:dind`, Docker uses the `vfs` storage driver which
+copies the filesystem on every run. This is a very disk-intensive operation
+which can be avoided if a different driver is used, for example `overlay`.
+
+1. Make sure a recent kernel is used, preferably `>= 4.2`.
+1. Check whether the `overlay` module is loaded:
+
+ ```
+ sudo lsmod | grep overlay
+ ```
+
+ If you see no result, then it isn't loaded. To load it use:
-Once you've built a Docker image, you can push it up to the built-in [GitLab Container Registry](../../user/project/container_registry.md). For example, if you're using
-docker-in-docker on your runners, this is how your `.gitlab-ci.yml` could look:
+ ```
+ sudo modprobe overlay
+ ```
+ If everything went fine, you need to make sure module is loaded on reboot.
+ On Ubuntu systems, this is done by editing `/etc/modules`. Just add the
+ following line into it:
+
+ ```
+ overlay
+ ```
+
+1. Use the driver by defining a variable at the top of your `.gitlab-ci.yml`:
+
+ ```
+ variables:
+ DOCKER_DRIVER: overlay
+ ```
+
+## Using the GitLab Container Registry
+
+> **Notes:**
+- This feature requires GitLab 8.8 and GitLab Runner 1.2.
+- Starting from GitLab 8.12, if you have 2FA enabled in your account, you need
+ to pass a personal access token instead of your password in order to login to
+ GitLab's Container Registry.
+
+Once you've built a Docker image, you can push it up to the built-in
+[GitLab Container Registry](../../user/project/container_registry.md). For example,
+if you're using docker-in-docker on your runners, this is how your `.gitlab-ci.yml`
+could look like:
```yaml
build:
@@ -307,10 +358,20 @@ deploy:
```
Some things you should be aware of when using the Container Registry:
-* You must log in to the container registry before running commands. Putting this in `before_script` will run it before each build job.
-* Using `docker build --pull` makes sure that Docker fetches any changes to base images before building just in case your cache is stale. It takes slightly longer, but means you don’t get stuck without security patches to base images.
-* Doing an explicit `docker pull` before each `docker run` makes sure to fetch the latest image that was just built. This is especially important if you are using multiple runners that cache images locally. Using the git SHA in your image tag makes this less necessary since each build will be unique and you shouldn't ever have a stale image, but it's still possible if you re-build a given commit after a dependency has changed.
-* You don't want to build directly to `latest` in case there are multiple builds happening simultaneously.
+
+- You must log in to the container registry before running commands. Putting
+ this in `before_script` will run it before each build job.
+- Using `docker build --pull` makes sure that Docker fetches any changes to base
+ images before building just in case your cache is stale. It takes slightly
+ longer, but means you don’t get stuck without security patches to base images.
+- Doing an explicit `docker pull` before each `docker run` makes sure to fetch
+ the latest image that was just built. This is especially important if you are
+ using multiple runners that cache images locally. Using the git SHA in your
+ image tag makes this less necessary since each build will be unique and you
+ shouldn't ever have a stale image, but it's still possible if you re-build a
+ given commit after a dependency has changed.
+- You don't want to build directly to `latest` in case there are multiple builds
+ happening simultaneously.
[docker-in-docker]: https://blog.docker.com/2013/09/docker-can-now-run-within-docker/
[docker-cap]: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index e070302fb82..bad0233a9ce 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -3,69 +3,535 @@
>**Note:**
Introduced in GitLab 8.9.
-## Environments
+During the development of software, there can be many stages until it's ready
+for public consumption. You sure want to first test your code and then deploy it
+in a testing or staging environment before you release it to the public. That
+way you can prevent bugs not only in your software, but in the deployment
+process as well.
-Environments are places where code gets deployed, such as staging or production.
-CI/CD [Pipelines] usually have one or more [jobs] that deploy to an environment.
-Defining environments in a project's `.gitlab-ci.yml` lets developers track
-[deployments] to these environments.
+GitLab CI is capable of not only testing or building your projects, but also
+deploying them in your infrastructure, with the added benefit of giving you a
+way to track your deployments. In other words, you can always know what is
+currently being deployed or has been deployed on your servers.
-## Deployments
+## Overview
-Deployments are created when [jobs] deploy versions of code to [environments].
+With environments, you can control the Continuous Deployment of your software
+all within GitLab. All you need to do is define them in your project's
+[`.gitlab-ci.yml`][yaml] as we will explore below. GitLab provides a full
+history of your deployments per every environment.
-### Checkout deployments locally
+Environments are like tags for your CI jobs, describing where code gets deployed.
+Deployments are created when [jobs] deploy versions of code to environments,
+so every environment can have one or more deployments. GitLab keeps track of
+your deployments, so you always know what is currently being deployed on your
+servers.
-Since 8.13, a reference in the git repository is saved for each deployment. So
-knowing what the state is of your current environments is only a `git fetch`
-away.
+To better understand how environments and deployments work, let's consider an
+example. We assume that you have already created a project in GitLab and set up
+a Runner. The example will cover the following:
-In your git config, append the `[remote "<your-remote>"]` block with an extra
-fetch line:
+- We are developing an application
+- We want to run tests and build our app on all branches
+- Our default branch is `master`
+- We deploy the app only when a pipeline on `master` branch is run
+
+Let's see how it all ties together.
+
+## Defining environments
+
+Let's consider the following `.gitlab-ci.yml` example:
+
+```yaml
+stages:
+ - test
+ - build
+ - deploy
+
+test:
+ stage: test
+ script: echo "Running tests"
+build:
+ stage: build
+ script: echo "Building the app"
+
+deploy_staging:
+ stage: deploy
+ script:
+ - echo "Deploy to staging server"
+ environment:
+ name: staging
+ url: https://staging.example.com
+ only:
+ - master
```
-fetch = +refs/environments/*:refs/remotes/origin/environments/*
+
+We have defined 3 [stages](yaml/README.md#stages):
+
+- test
+- build
+- deploy
+
+The jobs assigned to these stages will run in this order. If a job fails, then
+the builds that are assigned to the next stage won't run, rendering the pipeline
+as failed. In our case, the `test` job will run first, then the `build` and
+lastly the `deploy_staging`. With this, we ensure that first the tests pass,
+then our app is able to be built successfully, and lastly we deploy to the
+staging server.
+
+The `environment` keyword is just a hint for GitLab that this job actually
+deploys to this environment's `name`. It can also have a `url` which, as we
+will later see, is exposed in various places within GitLab. Each time a job that
+has an environment specified and succeeds, a deployment is recorded, remembering
+the Git SHA and environment name.
+
+>**Note:**
+Starting with GitLab 8.15, the environment name is exposed to the Runner in
+two forms: `$CI_ENVIRONMENT_NAME`, and `$CI_ENVIRONMENT_SLUG`. The first is
+the name given in `.gitlab-ci.yml` (with any variables expanded), while the
+second is a "cleaned-up" version of the name, suitable for use in URLs, DNS,
+etc.
+
+To sum up, with the above `.gitlab-ci.yml` we have achieved that:
+
+- All branches will run the `test` and `build` jobs.
+- The `deploy_staging` job will run [only](yaml/README.md#only) on the `master`
+ branch which means all merge requests that are created from branches don't
+ get to deploy to the staging server
+- When a merge request is merged, all jobs will run and the `deploy_staging`
+ in particular will deploy our code to a staging server while the deployment
+ will be recorded in an environment named `staging`.
+
+Let's now see how that information is exposed within GitLab.
+
+## Viewing the current status of an environment
+
+The environment list under your project's **Pipelines ➔ Environments**, is
+where you can find information of the last deployment status of an environment.
+
+Here's how the Environments page looks so far.
+
+![Staging environment view](img/environments_available_staging.png)
+
+There's a bunch of information there, specifically you can see:
+
+- The environment's name with a link to its deployments
+- The last deployment ID number and who performed it
+- The build ID of the last deployment with its respective job name
+- The commit information of the last deployment such as who committed, to what
+ branch and the Git SHA of the commit
+- The exact time the last deployment was performed
+- A button that takes you to the URL that you have defined under the
+ `environment` keyword in `.gitlab-ci.yml`
+- A button that re-deploys the latest deployment, meaning it runs the job
+ defined by the environment name for that specific commit
+
+>**Notes:**
+- While you can create environments manually in the web interface, we recommend
+ that you define your environments in `.gitlab-ci.yml` first. They will
+ be automatically created for you after the first deploy.
+- The environments page can only be viewed by Reporters and above. For more
+ information on the permissions, see the [permissions documentation][permissions].
+- Only deploys that happen after your `.gitlab-ci.yml` is properly configured
+ will show up in the "Environment" and "Last deployment" lists.
+
+The information shown in the Environments page is limited to the latest
+deployments, but as you may have guessed an environment can have multiple
+deployments.
+
+## Viewing the deployment history of an environment
+
+GitLab keeps track of your deployments, so you always know what is currently
+being deployed on your servers. That way you can have the full history of your
+deployments per every environment right in your browser. Clicking on an
+environment will show the history of its deployments. Assuming you have deployed
+multiple times already, here's how a specific environment's page looks like.
+
+![Deployments](img/deployments_view.png)
+
+We can see the same information as when in the Environments page, but this time
+all deployments are shown. As you may have noticed, apart from the **Re-deploy**
+button there are now **Rollback** buttons for each deployment. Let's see how
+that works.
+
+## Rolling back changes
+
+You can't control everything, so sometimes things go wrong. When that unfortunate
+time comes GitLab has you covered. Simply by clicking the **Rollback** button
+that can be found in the deployments page
+(**Pipelines ➔ Environments ➔ `environment name`**) you can relaunch the
+job with the commit associated with it.
+
+>**Note:**
+Bear in mind that your mileage will vary and it's entirely up to how you define
+the deployment process in the job's `script` whether the rollback succeeds or not.
+GitLab CI is just following orders.
+
+Thankfully that was the staging server that we had to rollback, and since we
+learn from our mistakes, we decided to not make the same again when we deploy
+to the production server. Enter manual actions for deployments.
+
+## Manually deploying to environments
+
+Turning a job from running automatically to a manual action is as simple as
+adding `when: manual` to it. To expand on our previous example, let's add
+another job that this time deploys our app to a production server and is
+tracked by a `production` environment. The `.gitlab-ci.yml` looks like this
+so far:
+
+```yaml
+stages:
+ - test
+ - build
+ - deploy
+
+test:
+ stage: test
+ script: echo "Running tests"
+
+build:
+ stage: build
+ script: echo "Building the app"
+
+deploy_staging:
+ stage: deploy
+ script:
+ - echo "Deploy to staging server"
+ environment:
+ name: staging
+ url: https://staging.example.com
+ only:
+ - master
+
+deploy_prod:
+ stage: deploy
+ script:
+ - echo "Deploy to production server"
+ environment:
+ name: production
+ url: https://example.com
+ when: manual
+ only:
+ - master
```
-## Defining environments
+The `when: manual` action exposes a play button in GitLab's UI and the
+`deploy_prod` job will only be triggered if and when we click that play button.
+You can find it in the pipeline, build, environment, and deployment views.
+
+| Pipelines | Single pipeline | Environments | Deployments | Builds |
+| --------- | ----------------| ------------ | ----------- | -------|
+| ![Pipelines manual action](img/environments_manual_action_pipelines.png) | ![Pipelines manual action](img/environments_manual_action_single_pipeline.png) | ![Environments manual action](img/environments_manual_action_environments.png) | ![Deployments manual action](img/environments_manual_action_deployments.png) | ![Builds manual action](img/environments_manual_action_builds.png) |
+
+Clicking on the play button in either of these places will trigger the
+`deploy_prod` job, and the deployment will be recorded under a new
+environment named `production`.
+
+>**Note:**
+Remember that if your environment's name is `production` (all lowercase), then
+it will get recorded in [Cycle Analytics](../user/project/cycle_analytics.md).
+Double the benefit!
+
+While this is fine for deploying to some stable environments like staging or
+production, what happens for branches? So far we haven't defined anything
+regarding deployments for branches other than `master`. Dynamic environments
+will help us achieve that.
-You can create and delete environments manually in the web interface, but we
-recommend that you define your environments in `.gitlab-ci.yml` first, which
-will automatically create environments for you after the first deploy.
+## Dynamic environments
-The `environment` is just a hint for GitLab that this job actually deploys to
-this environment. Each time the job succeeds, a deployment is recorded,
-remembering the git SHA and environment.
+As the name suggests, it is possible to create environments on the fly by just
+declaring their names dynamically in `.gitlab-ci.yml`. Dynamic environments is
+the basis of [Review apps](review_apps/index.md).
-Add something like this to your `.gitlab-ci.yml`:
+GitLab Runner exposes various [environment variables][variables] when a job runs,
+and as such, you can use them as environment names. Let's add another job in
+our example which will deploy to all branches except `master`:
+
+```yaml
+deploy_review:
+ stage: deploy
+ script:
+ - echo "Deploy a review app"
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ url: https://$CI_BUILD_REF_SLUG.review.example.com
+ only:
+ - branches
+ except:
+ - master
```
-production:
+
+Let's break it down in pieces. The job's name is `deploy_review` and it runs
+on the `deploy` stage. The `script` at this point is fictional, you'd have to
+use your own based on your deployment. Then, we set the `environment` with the
+`environment:name` being `review/$CI_BUILD_REF_NAME`. Now that's an interesting
+one. Since the [environment name][env-name] can contain slashes (`/`), we can
+use this pattern to distinguish between dynamic environments and the regular
+ones.
+
+So, the first part is `review`, followed by a `/` and then `$CI_BUILD_REF_NAME`
+which takes the value of the branch name. Since `$CI_BUILD_REF_NAME` itself may
+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
+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`
+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`
+
+Last but not least, we tell the job to run [`only`][only] on branches
+[`except`][only] master.
+
+>**Note:**
+You are not bound to use the same prefix or only slashes in the dynamic
+environments' names (`/`), but as we will see later, this will enable the
+[grouping similar environments](#grouping-similar-environments) feature.
+
+The whole `.gitlab-ci.yml` looks like this so far:
+
+```yaml
+stages:
+ - test
+ - build
+ - deploy
+
+test:
+ stage: test
+ script: echo "Running tests"
+
+build:
+ stage: build
+ script: echo "Building the app"
+
+deploy_review:
+ stage: deploy
+ script:
+ - echo "Deploy a review app"
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ url: https://$CI_ENVIRONMENT_SLUG.example.com
+ only:
+ - branches
+ except:
+ - master
+
+deploy_staging:
stage: deploy
- script: dpl...
- environment: production
+ script:
+ - echo "Deploy to staging server"
+ environment:
+ name: staging
+ url: https://staging.example.com
+ only:
+ - master
+
+deploy_prod:
+ stage: deploy
+ script:
+ - echo "Deploy to production server"
+ environment:
+ name: production
+ url: https://example.com
+ when: manual
+ only:
+ - master
```
-See full [documentation](yaml/README.md#environment).
+A more realistic example would include copying files to a location where a
+webserver (NGINX) could then read and serve. The example below will copy the
+`public` directory to `/srv/nginx/$CI_BUILD_REF_SLUG/public`:
-## Seeing environment status
+```yaml
+review_app:
+ stage: deploy
+ script:
+ - rsync -av --delete public /srv/nginx/$CI_BUILD_REF_SLUG
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ url: https://$CI_BUILD_REF_SLUG.example.com
+```
-You can find the environment list under **Pipelines > Environments** for your
-project. You'll see the git SHA and date of the last deployment to each
-environment defined.
+It is assumed that the user has already setup NGINX and GitLab Runner in the
+server this job will run on.
>**Note:**
-Only deploys that happen after your `.gitlab-ci.yml` is properly configured will
-show up in the environments and deployments lists.
+Be sure to check out the [limitations](#limitations) section for some edge
+cases regarding naming of your branches and Review Apps.
+
+---
+
+The development workflow would now be:
+
+- Developer creates a branch locally
+- Developer makes changes, commits and pushes the branch to GitLab
+- Developer creates a merge request
-## Seeing deployment history
+Behind the scenes:
-Clicking on an environment will show the history of deployments.
+- GitLab Runner picks up the changes and starts running the jobs
+- The jobs run sequentially as defined in `stages`
+ - First, the tests pass
+ - Then, the build begins and successfully also passes
+ - Lastly, the app is deployed to an environment with a name specific to the
+ branch
+
+So now, every branch gets its own environment and is deployed to its own place
+with the added benefit of having a [history of deployments](#viewing-the-deployment-history-of-an-environment)
+and also being able to [rollback changes](#rolling-back-changes) if needed.
+Let's briefly see where URL that's defined in the environments is exposed.
+
+## Making use of the environment URL
+
+The environment URL is exposed in a few places within GitLab.
+
+| In a merge request widget as a link | In the Environments view as a button | In the Deployments view as a button |
+| -------------------- | ------------ | ----------- |
+| ![Environment URL in merge request](img/environments_mr_review_app.png) | ![Environment URL in environments](img/environments_link_url.png) | ![Environment URL in deployments](img/environments_link_url_deployments.png) |
+
+If a merge request is eventually merged to the default branch (in our case
+`master`) and that branch also deploys to an environment (in our case `staging`
+and/or `production`) you can see this information in the merge request itself.
+
+![Environment URLs in merge request](img/environments_link_url_mr.png)
+
+---
+
+We now have a full development cycle, where our app is tested, built, deployed
+as a Review app, deployed to a staging server once the merge request is merged,
+and finally manually deployed to the production server. What we just described
+is a single workflow, but imagine tens of developers working on a project
+at the same time. They each push to their branches, and dynamic environments are
+created all the time. In that case, we probably need to do some clean up. Read
+next how environments can be stopped.
+
+## Stopping an environment
+
+By stopping an environment, you are effectively terminating its recording of the
+deployments that happen in it.
+
+A branch is associated with an environment when the CI pipeline that is created
+for this branch, was recently deployed to this environment. You can think of
+the CI pipeline as the glue between the branch and the environment:
+`branch ➔ CI pipeline ➔ environment`.
+
+There is a special case where environments can be manually stopped. That can
+happen if you provide another job for that matter. The syntax is a little
+tricky since a job calls another job to do the job.
+
+Consider the following example where the `deploy_review` calls the `stop_review`
+to clean up and stop the environment:
+
+```yaml
+deploy_review:
+ stage: deploy
+ script:
+ - echo "Deploy a review app"
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ url: https://$CI_ENVIRONMENT_SLUG.example.com
+ on_stop: stop_review
+ only:
+ - branches
+ except:
+ - master
+
+stop_review:
+ variables:
+ GIT_STRATEGY: none
+ script:
+ - echo "Remove review app"
+ when: manual
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ action: stop
+```
+
+Setting the [`GIT_STRATEGY`][git-strategy] to `none` is necessary on the
+`stop_review` job so that the [GitLab Runner] won't try to checkout the code
+after the branch is deleted.
>**Note:**
-Only deploys that happen after your `.gitlab-ci.yml` is properly configured will
-show up in the environments and deployments lists.
+Starting with GitLab 8.14, dynamic environments will be stopped automatically
+when their associated branch is deleted.
+
+When you have an environment that has a stop action defined (typically when
+the environment describes a review app), GitLab will automatically trigger a
+stop action when the associated branch is deleted.
+
+You can read more in the [`.gitlab-ci.yml` reference][onstop].
+
+## Grouping similar environments
+
+> [Introduced][ce-7015] in GitLab 8.14.
+
+As we've seen in the [dynamic environments](#dynamic-environments), you can
+prepend their name with a word, then followed by a `/` and finally the branch
+name which is automatically defined by the `CI_BUILD_REF_NAME` variable.
+
+In short, environments that are named like `type/foo` are presented under a
+group named `type`.
+
+In our minimal example, we name the environments `review/$CI_BUILD_REF_NAME`
+where `$CI_BUILD_REF_NAME` is the branch name:
+
+```yaml
+deploy_review:
+ stage: deploy
+ script:
+ - echo "Deploy a review app"
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+```
+
+In that case, if you visit the Environments page, and provided the branches
+exist, you should see something like:
+
+![Environment groups](img/environments_dynamic_groups.png)
+
+## Checkout deployments locally
+
+Since 8.13, a reference in the git repository is saved for each deployment, so
+knowing the state of your current environments is only a `git fetch` away.
+
+In your git config, append the `[remote "<your-remote>"]` block with an extra
+fetch line:
+
+```
+fetch = +refs/environments/*:refs/remotes/origin/environments/*
+```
+
+## Limitations
+
+1. You are limited to use only the [CI predefined variables][variables] in the
+ `environment: name`. If you try to re-use variables defined inside `script`
+ as part of the environment name, it will not work.
+
+## Further reading
+
+Below are some links you may find interesting:
+
+- [The `.gitlab-ci.yml` definition of environments](yaml/README.md#environment)
+- [A blog post on Deployments & Environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
+- [Review Apps - Use dynamic environments to deploy your code for every branch](review_apps/index.md)
[Pipelines]: pipelines.md
[jobs]: yaml/README.md#jobs
+[yaml]: yaml/README.md
[environments]: #environments
[deployments]: #deployments
+[permissions]: ../user/permissions.md
+[variables]: variables/README.md
+[env-name]: yaml/README.md#environment-name
+[only]: yaml/README.md#only-and-except
+[onstop]: yaml/README.md#environment-on_stop
+[ce-7015]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7015
+[gitlab-flow]: ../workflow/gitlab_flow.md
+[gitlab runner]: https://docs.gitlab.com/runner/
+[git-strategy]: yaml/README.md#git-strategy
diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md
index 175e9d79904..82ffb841729 100644
--- a/doc/ci/examples/php.md
+++ b/doc/ci/examples/php.md
@@ -40,7 +40,7 @@ repository with the following content:
#!/bin/bash
# We need to install dependencies only for Docker
-[[ ! -e /.dockerenv ]] && [[ ! -e /.dockerinit ]] && exit 0
+[[ ! -e /.dockerenv ]] && exit 0
set -xe
diff --git a/doc/ci/examples/test-scala-application.md b/doc/ci/examples/test-scala-application.md
index 7412fdbbc78..85f8849fa99 100644
--- a/doc/ci/examples/test-scala-application.md
+++ b/doc/ci/examples/test-scala-application.md
@@ -1,11 +1,11 @@
-## Test a Scala application
+# Test and deploy to Heroku a Scala application
This example demonstrates the integration of Gitlab CI with Scala
applications using SBT. Checkout the example
[project](https://gitlab.com/gitlab-examples/scala-sbt) and
[build status](https://gitlab.com/gitlab-examples/scala-sbt/builds).
-### Add `.gitlab-ci.yml` file to project
+## Add `.gitlab-ci.yml` file to project
The following `.gitlab-ci.yml` should be added in the root of your
repository to trigger CI:
@@ -13,10 +13,14 @@ repository to trigger CI:
``` yaml
image: java:8
+stages:
+ - test
+ - deploy
+
before_script:
- apt-get update -y
- apt-get install apt-transport-https -y
- # Install SBT
+ ## Install SBT
- echo "deb http://dl.bintray.com/sbt/debian /" | tee -a /etc/apt/sources.list.d/sbt.list
- apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 642AC823
- apt-get update -y
@@ -24,8 +28,17 @@ before_script:
- sbt sbt-version
test:
+ stage: test
script:
- sbt clean coverage test coverageReport
+
+deploy:
+ stage: deploy
+ script:
+ - apt-get update -yq
+ - apt-get install rubygems ruby-dev -y
+ - gem install dpl
+ - dpl --provider=heroku --app=gitlab-play-sample-app --api-key=$HEROKU_API_KEY
```
The `before_script` installs [SBT](http://www.scala-sbt.org/) and
@@ -33,15 +46,31 @@ displays the version that is being used. The `test` stage executes SBT
to compile and test the project.
[scoverage](https://github.com/scoverage/sbt-scoverage) is used as an SBT
plugin to measure test coverage.
+The `deploy` stage automatically deploys the project to Heroku using dpl.
You can use other versions of Scala and SBT by defining them in
`build.sbt`.
-### Display test coverage in build
+## Display test coverage in build
Add the `Coverage was \[\d+.\d+\%\]` regular expression in the
-**Settings > Edit Project > Test coverage parsing** project setting to
-retrieve the test coverage rate from the build trace and have it
+**Settings ➔ Edit Project ➔ Test coverage parsing** project setting to
+retrieve the [test coverage] rate from the build trace and have it
displayed with your builds.
**Builds** must be enabled for this option to appear.
+
+## Heroku application
+
+A Heroku application is required. You can create one through the
+[Dashboard](https://dashboard.heroku.com/). Substitute `gitlab-play-sample-app`
+in the `.gitlab-ci.yml` file with your application's name.
+
+## Heroku API key
+
+You can look up your Heroku API key in your
+[account](https://dashboard.heroku.com/account). Add a secure [variable] with
+this value in **Project ➔ Variables** with key `HEROKU_API_KEY`.
+
+[variable]: ../variables/README.md#user-defined-variables-secure-variables
+[test coverage]: ../../user/project/pipelines/settings.md#test-coverage-report-badge
diff --git a/doc/ci/git_submodules.md b/doc/ci/git_submodules.md
new file mode 100644
index 00000000000..1d782200cca
--- /dev/null
+++ b/doc/ci/git_submodules.md
@@ -0,0 +1,86 @@
+# Using Git submodules with GitLab CI
+
+> **Notes:**
+- GitLab 8.12 introduced a new [CI build permissions model][newperms] and you
+ are encouraged to upgrade your GitLab instance if you haven't done already.
+ If you are **not** using GitLab 8.12 or higher, you would need to work your way
+ around submodules in order to access the sources of e.g., `gitlab.com/group/project`
+ with the use of [SSH keys](ssh_keys/README.md).
+- With GitLab 8.12 onward, your permissions are used to evaluate what a CI build
+ can access. More information about how this system works can be found in the
+ [Build permissions model](../user/permissions.md#builds-permissions).
+- The HTTP(S) Git protocol [must be enabled][gitpro] in your GitLab instance.
+
+## Configuring the `.gitmodules` file
+
+If dealing with [Git submodules][gitscm], your project will probably have a file
+named `.gitmodules`.
+
+Let's consider the following example:
+
+1. Your project is located at `https://gitlab.com/secret-group/my-project`.
+1. To checkout your sources you usually use an SSH address like
+ `git@gitlab.com:secret-group/my-project.git`.
+1. Your project depends on `https://gitlab.com/group/project`, which you want
+ to include as a submodule.
+
+If you are using GitLab 8.12+ and your submodule is on the same GitLab server,
+you must update your `.gitmodules` file to use **relative URLs**.
+Since Git allows the usage of relative URLs for your `.gitmodules` configuration,
+this easily allows you to use HTTP(S) for cloning all your CI builds and SSH
+for all your local checkouts. The `.gitmodules` would look like:
+
+```ini
+[submodule "project"]
+ path = project
+ url = ../../group/project.git
+```
+
+The above configuration will instruct Git to automatically deduce the URL that
+should be used when cloning sources. Whether you use HTTP(S) or SSH, Git will use
+that same channel and it will allow to make all your CI builds use HTTP(S)
+(because GitLab CI only uses HTTP(S) for cloning your sources), and all your local
+clones will continue using SSH.
+
+For all other submodules not located on the same GitLab server, use the full
+HTTP(S) protocol URL:
+
+```ini
+[submodule "project-x"]
+ path = project-x
+ url = https://gitserver.com/group/project-x.git
+```
+
+Once `.gitmodules` is correctly configured, you can move on to
+[configuring your `.gitlab-ci.yml`](#using-git-submodules-in-your-ci-jobs).
+
+## Using Git submodules in your CI jobs
+
+There are a few steps you need to take in order to make submodules work
+correctly with your CI builds:
+
+1. First, make sure you have used [relative URLs](#configuring-the-gitmodules-file)
+ for the submodules located in the same GitLab server.
+1. Then, use `git submodule sync/update` in `before_script`:
+
+ ```yaml
+ before_script:
+ - git submodule sync --recursive
+ - git submodule update --init --recursive
+ ```
+
+ `--recursive` should be used in either both or none (`sync/update`) depending on
+ whether you have recursive submodules.
+
+The rationale to set the `sync` and `update` in `before_script` is because of
+the way Git submodules work. On a fresh Runner workspace, Git will set the
+submodule URL including the token in `.git/config`
+(or `.git/modules/<submodule>/config`) based on `.gitmodules` and the current
+remote URL. On subsequent builds on the same Runner, `.git/config` is cached
+and already contains a full URL for the submodule, corresponding to the previous
+build, and to **a token from a previous build**. `sync` allows to force updating
+the full URL.
+
+[gitpro]: ../user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols
+[gitscm]: https://git-scm.com/book/en/v2/Git-Tools-Submodules "Git submodules documentation"
+[newperms]: ../user/project/new_ci_build_permissions_model.md
diff --git a/doc/ci/img/builds_tab.png b/doc/ci/img/builds_tab.png
index 35780e277ae..2d7eec8a949 100644
--- a/doc/ci/img/builds_tab.png
+++ b/doc/ci/img/builds_tab.png
Binary files differ
diff --git a/doc/ci/img/deployments_view.png b/doc/ci/img/deployments_view.png
new file mode 100644
index 00000000000..7ded0c97b72
--- /dev/null
+++ b/doc/ci/img/deployments_view.png
Binary files differ
diff --git a/doc/ci/img/environments_available_staging.png b/doc/ci/img/environments_available_staging.png
new file mode 100644
index 00000000000..5c031ad0d9d
--- /dev/null
+++ b/doc/ci/img/environments_available_staging.png
Binary files differ
diff --git a/doc/ci/img/environments_dynamic_groups.png b/doc/ci/img/environments_dynamic_groups.png
new file mode 100644
index 00000000000..0f42b368c5b
--- /dev/null
+++ b/doc/ci/img/environments_dynamic_groups.png
Binary files differ
diff --git a/doc/ci/img/environments_link_url.png b/doc/ci/img/environments_link_url.png
new file mode 100644
index 00000000000..44010f6aa6f
--- /dev/null
+++ b/doc/ci/img/environments_link_url.png
Binary files differ
diff --git a/doc/ci/img/environments_link_url_deployments.png b/doc/ci/img/environments_link_url_deployments.png
new file mode 100644
index 00000000000..4f90143527a
--- /dev/null
+++ b/doc/ci/img/environments_link_url_deployments.png
Binary files differ
diff --git a/doc/ci/img/environments_link_url_mr.png b/doc/ci/img/environments_link_url_mr.png
new file mode 100644
index 00000000000..64f134e0b0d
--- /dev/null
+++ b/doc/ci/img/environments_link_url_mr.png
Binary files differ
diff --git a/doc/ci/img/environments_manual_action_builds.png b/doc/ci/img/environments_manual_action_builds.png
new file mode 100644
index 00000000000..e7cf63a1031
--- /dev/null
+++ b/doc/ci/img/environments_manual_action_builds.png
Binary files differ
diff --git a/doc/ci/img/environments_manual_action_deployments.png b/doc/ci/img/environments_manual_action_deployments.png
new file mode 100644
index 00000000000..2b3f6f3edad
--- /dev/null
+++ b/doc/ci/img/environments_manual_action_deployments.png
Binary files differ
diff --git a/doc/ci/img/environments_manual_action_environments.png b/doc/ci/img/environments_manual_action_environments.png
new file mode 100644
index 00000000000..e0c07604e7f
--- /dev/null
+++ b/doc/ci/img/environments_manual_action_environments.png
Binary files differ
diff --git a/doc/ci/img/environments_manual_action_pipelines.png b/doc/ci/img/environments_manual_action_pipelines.png
new file mode 100644
index 00000000000..82bbae88027
--- /dev/null
+++ b/doc/ci/img/environments_manual_action_pipelines.png
Binary files differ
diff --git a/doc/ci/img/environments_manual_action_single_pipeline.png b/doc/ci/img/environments_manual_action_single_pipeline.png
new file mode 100644
index 00000000000..36337cb1870
--- /dev/null
+++ b/doc/ci/img/environments_manual_action_single_pipeline.png
Binary files differ
diff --git a/doc/ci/img/environments_mr_review_app.png b/doc/ci/img/environments_mr_review_app.png
new file mode 100644
index 00000000000..7bff84362a3
--- /dev/null
+++ b/doc/ci/img/environments_mr_review_app.png
Binary files differ
diff --git a/doc/ci/img/environments_view.png b/doc/ci/img/environments_view.png
new file mode 100644
index 00000000000..821352188ef
--- /dev/null
+++ b/doc/ci/img/environments_view.png
Binary files differ
diff --git a/doc/ci/img/features_settings.png b/doc/ci/img/features_settings.png
index 38d7036f606..c159253d1c9 100644
--- a/doc/ci/img/features_settings.png
+++ b/doc/ci/img/features_settings.png
Binary files differ
diff --git a/doc/ci/img/pipelines.png b/doc/ci/img/pipelines.png
new file mode 100644
index 00000000000..5937e9d99c8
--- /dev/null
+++ b/doc/ci/img/pipelines.png
Binary files differ
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index 7d100a4fd93..03b9c4bb444 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -11,6 +11,8 @@ concurrent [Runners]), and if they all succeed, the pipeline moves on to the
next stage. If one of the builds fails, the next stage is not (usually)
executed.
+![Pipelines example](img/pipelines.png)
+
## Builds
Builds are individual runs of [jobs]. Not to be confused with a `build` job or
diff --git a/doc/ci/quick_start/img/build_log.png b/doc/ci/quick_start/img/build_log.png
index b53a6cd86b0..87643d62d58 100644
--- a/doc/ci/quick_start/img/build_log.png
+++ b/doc/ci/quick_start/img/build_log.png
Binary files differ
diff --git a/doc/ci/quick_start/img/builds_status.png b/doc/ci/quick_start/img/builds_status.png
index 47862761ffe..d287ae3064f 100644
--- a/doc/ci/quick_start/img/builds_status.png
+++ b/doc/ci/quick_start/img/builds_status.png
Binary files differ
diff --git a/doc/ci/quick_start/img/new_commit.png b/doc/ci/quick_start/img/new_commit.png
index a53562ce328..29c2fea5d6d 100644
--- a/doc/ci/quick_start/img/new_commit.png
+++ b/doc/ci/quick_start/img/new_commit.png
Binary files differ
diff --git a/doc/ci/quick_start/img/pipelines_status.png b/doc/ci/quick_start/img/pipelines_status.png
index 6bc97bb739c..53ccc49bd66 100644
--- a/doc/ci/quick_start/img/pipelines_status.png
+++ b/doc/ci/quick_start/img/pipelines_status.png
Binary files differ
diff --git a/doc/ci/quick_start/img/runners_activated.png b/doc/ci/quick_start/img/runners_activated.png
index 23261123b18..5ce6fe8e17c 100644
--- a/doc/ci/quick_start/img/runners_activated.png
+++ b/doc/ci/quick_start/img/runners_activated.png
Binary files differ
diff --git a/doc/ci/quick_start/img/single_commit_status_pending.png b/doc/ci/quick_start/img/single_commit_status_pending.png
index ccf3ac957bb..91fc9011847 100644
--- a/doc/ci/quick_start/img/single_commit_status_pending.png
+++ b/doc/ci/quick_start/img/single_commit_status_pending.png
Binary files differ
diff --git a/doc/ci/quick_start/img/status_pending.png b/doc/ci/quick_start/img/status_pending.png
index 9feacf0c961..cbd44a189d3 100644
--- a/doc/ci/quick_start/img/status_pending.png
+++ b/doc/ci/quick_start/img/status_pending.png
Binary files differ
diff --git a/doc/ci/review_apps/img/review_apps_preview_in_mr.png b/doc/ci/review_apps/img/review_apps_preview_in_mr.png
new file mode 100644
index 00000000000..0300392f24b
--- /dev/null
+++ b/doc/ci/review_apps/img/review_apps_preview_in_mr.png
Binary files differ
diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md
new file mode 100644
index 00000000000..c679ea4e298
--- /dev/null
+++ b/doc/ci/review_apps/index.md
@@ -0,0 +1,124 @@
+# Getting started with Review Apps
+
+>
+- [Introduced][ce-21971] in GitLab 8.12. Further additions were made in GitLab
+ 8.13 and 8.14.
+- Inspired by [Heroku's Review Apps][heroku-apps] which itself was inspired by
+ [Fourchette].
+
+The basis of Review Apps is the [dynamic environments] which allow you to create
+a new environment (dynamically) for each one of your branches.
+
+A Review App can then be visible as a link when you visit the [merge request]
+relevant to the branch. That way, you are able to see live all changes introduced
+by the merge request changes. Reviewing anything, from performance to interface
+changes, becomes much easier with a live environment and as such, Review Apps
+can make a huge impact on your development flow.
+
+They mostly make sense to be used with web applications, but you can use them
+any way you'd like.
+
+## Overview
+
+Simply put, a Review App is a mapping of a branch with an environment as there
+is a 1:1 relation between them.
+
+Here's an example of what it looks like when viewing a merge request with a
+dynamically set environment.
+
+![Review App in merge request](img/review_apps_preview_in_mr.png)
+
+In the image above you can see that the `add-new-line` branch was successfully
+built and deployed under a dynamic environment and can be previewed with an
+also dynamically URL.
+
+The details of the Review Apps implementation depend widely on your real
+technology stack and on your deployment process. The simplest case is to
+deploy a simple static HTML website, but it will not be that straightforward
+when your app is using a database for example. To make a branch be deployed
+on a temporary instance and booting up this instance with all required software
+and services automatically on the fly is not a trivial task. However, it is
+doable, especially if you use Docker, or at least a configuration management
+tool like Chef, Puppet, Ansible or Salt.
+
+## Prerequisites
+
+To get a better understanding of Review Apps, you must first learn how
+environments and deployments work. The following docs will help you grasp that
+knowledge:
+
+1. First, learn about [environments][] and their role in the development workflow.
+1. Then make a small stop to learn about [CI variables][variables] and how they
+ can be used in your CI jobs.
+1. Next, explore the [`environment` syntax][yaml-env] as defined in `.gitlab-ci.yml`.
+ This will be your primary reference when you are finally comfortable with
+ how environments work.
+1. Additionally, find out about [manual actions][] and how you can use them to
+ deploy to critical environments like production with the push of a button.
+1. And as a last step, follow the [example tutorials](#examples) which will
+ guide you step by step to set up the infrastructure and make use of
+ Review Apps.
+
+## Configuration
+
+The configuration of Review apps depends on your technology stack and your
+infrastructure. Read the [dynamic environments] documentation to understand
+how to define and create them.
+
+## Creating and destroying Review Apps
+
+The creation and destruction of a Review App is defined in `.gitlab-ci.yml`
+at a job level under the `environment` keyword.
+
+Check the [environments] documentation how to do so.
+
+## A simple workflow
+
+The process of adding Review Apps in your workflow would look like:
+
+1. Set up the infrastructure to host and deploy the Review Apps.
+1. [Install][install-runner] and [configure][conf-runner] a Runner that does
+ the deployment.
+1. Set up a job in `.gitlab-ci.yml` that uses the predefined
+ [predefined CI environment variable][variables] `${CI_BUILD_REF_NAME}` to
+ create dynamic environments and restrict it to run only on branches.
+1. Optionally set a job that [manually stops][manual-env] the Review Apps.
+
+From there on, you would follow the branched Git flow:
+
+1. Push a branch and let the Runner deploy the Review App based on the `script`
+ definition of the dynamic environment job.
+1. Wait for the Runner to build and/or deploy your web app.
+1. Click on the link that's present in the MR related to the branch and see the
+ changes live.
+
+## Limitations
+
+Check the [environments limitations](../environments.md#limitations).
+
+## Examples
+
+A list of examples used with Review Apps can be found below:
+
+- [Use with NGINX][app-nginx] - Use NGINX and the shell executor of GitLab Runner
+ to deploy a simple HTML website.
+
+And below is a soon to be added examples list:
+
+- Use with Amazon S3
+- Use on Heroku with dpl
+- Use with OpenShift/kubernetes
+
+[app-nginx]: https://gitlab.com/gitlab-examples/review-apps-nginx
+[ce-21971]: https://gitlab.com/gitlab-org/gitlab-ce/issues/21971
+[dynamic environments]: ../environments.md#dynamic-environments
+[environments]: ../environments.md
+[fourchette]: https://github.com/rainforestapp/fourchette
+[heroku-apps]: https://devcenter.heroku.com/articles/github-integration-review-apps
+[manual actions]: ../environments.md#manual-actions
+[merge request]: ../../user/project/merge_requests.md
+[variables]: ../variables/README.md
+[yaml-env]: ../yaml/README.md#environment
+[install-runner]: https://docs.gitlab.com/runner/install/
+[conf-runner]: https://docs.gitlab.com/runner/commands/
+[manual-env]: ../environments.md#stopping-an-environment
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 84048f1d25f..efca05af7b8 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -6,7 +6,7 @@
GitLab 8.12 has a completely redesigned build permissions system.
Read all about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#build-triggers).
-Triggers can be used to force a rebuild of a specific branch, tag or commit,
+Triggers can be used to force a rebuild of a specific `ref` (branch or tag)
with an API call.
## Add a trigger
@@ -29,6 +29,10 @@ irreversible.
## Trigger a build
+> **Note**:
+Valid refs are only the branches and tags. If you pass a commit SHA as a ref,
+it will not trigger a build.
+
To trigger a build you need to send a `POST` request to GitLab's API endpoint:
```
@@ -36,8 +40,8 @@ POST /projects/:id/trigger/builds
```
The required parameters are the trigger's `token` and the Git `ref` on which
-the trigger will be performed. Valid refs are the branch, the tag or the commit
-SHA. The `:id` of a project can be found by [querying the API](../../api/projects.md)
+the trigger will be performed. Valid refs are the branch and the tag. The `:id`
+of a project can be found by [querying the API](../../api/projects.md)
or by visiting the **Triggers** page which provides self-explanatory examples.
When a rebuild is triggered, the information is exposed in GitLab's UI under
@@ -58,6 +62,22 @@ below.
See the [Examples](#examples) section for more details on how to actually
trigger a rebuild.
+## Trigger a build from webhook
+
+> Introduced in GitLab 8.14.
+
+To trigger a build from webhook of another project you need to add the following
+webhook url for Push and Tag push events:
+
+```
+https://gitlab.example.com/api/v3/projects/:id/ref/:ref/trigger/builds?token=TOKEN
+```
+
+> **Note**:
+- `ref` should be passed as part of url in order to take precedence over `ref`
+ from webhook body that designates the branchref that fired the trigger in the source repository.
+- `ref` should be url encoded if contains slashes.
+
## Pass build variables to a trigger
You can pass any number of arbitrary variables in the trigger API call and they
@@ -169,6 +189,14 @@ curl --request POST \
https://gitlab.example.com/api/v3/projects/9/trigger/builds
```
+### Using webhook to trigger builds
+
+You can add the following webhook to another project in order to trigger a build:
+
+```
+https://gitlab.example.com/api/v3/projects/9/ref/master/trigger/builds?token=TOKEN&variables[UPLOAD_TO_S3]=true
+```
+
### Using cron to trigger nightly builds
Whether you craft a script or just run cURL directly, you can trigger builds
diff --git a/doc/ci/triggers/img/builds_page.png b/doc/ci/triggers/img/builds_page.png
index c2cf4b1852c..fded5839f76 100644
--- a/doc/ci/triggers/img/builds_page.png
+++ b/doc/ci/triggers/img/builds_page.png
Binary files differ
diff --git a/doc/ci/triggers/img/trigger_single_build.png b/doc/ci/triggers/img/trigger_single_build.png
index fa86f0fee3d..c4a5550d640 100644
--- a/doc/ci/triggers/img/trigger_single_build.png
+++ b/doc/ci/triggers/img/trigger_single_build.png
Binary files differ
diff --git a/doc/ci/triggers/img/trigger_variables.png b/doc/ci/triggers/img/trigger_variables.png
index b2fcc65d304..65fe1ea9ab6 100644
--- a/doc/ci/triggers/img/trigger_variables.png
+++ b/doc/ci/triggers/img/trigger_variables.png
Binary files differ
diff --git a/doc/ci/triggers/img/triggers_page.png b/doc/ci/triggers/img/triggers_page.png
index 438f285ae2d..56d13905ce6 100644
--- a/doc/ci/triggers/img/triggers_page.png
+++ b/doc/ci/triggers/img/triggers_page.png
Binary files differ
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index a4c3a731a20..baa5fc67816 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -1,22 +1,31 @@
-## Variables
+# Variables
-When receiving a build from GitLab CI, the runner prepares the build environment.
-It starts by setting a list of **predefined variables** (Environment Variables) and a list of **user-defined variables**
+When receiving a build from GitLab CI, the [Runner] prepares the build environment.
+It starts by setting a list of **predefined variables** (environment variables)
+and a list of **user-defined variables**.
-The variables can be overwritten. They take precedence over each other in this order:
-1. Trigger variables
-1. Secure variables
-1. YAML-defined job-level variables
-1. YAML-defined global variables
-1. Predefined variables
+## Priority of variables
-For example, if you define:
-1. `API_TOKEN=SECURE` as Secure Variable
-1. `API_TOKEN=YAML` as YAML-defined variable
+The variables can be overwritten and they take precedence over each other in
+this order:
-The `API_TOKEN` will take the Secure Variable value: `SECURE`.
+1. [Trigger variables][triggers] (take precedence over all)
+1. [Secret variables](#secret-variables)
+1. YAML-defined [job-level variables](../yaml/README.md#job-variables)
+1. YAML-defined [global variables](../yaml/README.md#variables)
+1. [Deployment variables](#deployment-variables)
+1. [Predefined variables](#predefined-variables-environment-variables) (are the
+ lowest in the chain)
-### Predefined variables (Environment Variables)
+For example, if you define `API_TOKEN=secure` as a secret variable and
+`API_TOKEN=yaml` in your `.gitlab-ci.yml`, the `API_TOKEN` will take the value
+`secure` as the secret variables are higher in the chain.
+
+## Predefined variables (Environment variables)
+
+Some of the predefined environment variables are available only if a minimum
+version of [GitLab Runner][runner] is used. Consult the table below to find the
+version of Runner required.
| Variable | GitLab | Runner | Description |
|-------------------------|--------|--------|-------------|
@@ -32,6 +41,7 @@ The `API_TOKEN` will take the Secure Variable value: `SECURE`.
| **CI_BUILD_NAME** | all | 0.5 | The name of the build as defined in `.gitlab-ci.yml` |
| **CI_BUILD_STAGE** | all | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` |
| **CI_BUILD_REF_NAME** | all | all | The branch or tag name for which project is built |
+| **CI_BUILD_REF_SLUG** | 8.15 | all | `$CI_BUILD_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
| **CI_BUILD_REPO** | all | all | The URL to clone the Git repository |
| **CI_BUILD_TRIGGERED** | all | 0.5 | The flag to indicate that build was [triggered] |
| **CI_BUILD_MANUAL** | 8.12 | all | The flag to indicate that build was manually started |
@@ -43,6 +53,8 @@ The `API_TOKEN` will take the Secure Variable value: `SECURE`.
| **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name |
| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project |
| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the build is run |
+| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this build |
+| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. |
| **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry |
| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project |
| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used |
@@ -52,7 +64,6 @@ The `API_TOKEN` will take the Secure Variable value: `SECURE`.
| **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the build |
| **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the build |
-**Some of the variables are only available when using runner with at least defined version.**
Example values:
@@ -60,7 +71,7 @@ Example values:
export CI_BUILD_ID="50"
export CI_BUILD_REF="1ecfd275763eff1d6b4844ea3168962458c9f27a"
export CI_BUILD_REF_NAME="master"
-export CI_BUILD_REPO="https://gitab-ci-token:abcde-1234ABCD5678ef@gitlab.com/gitlab-org/gitlab-ce.git"
+export CI_BUILD_REPO="https://gitab-ci-token:abcde-1234ABCD5678ef@example.com/gitlab-org/gitlab-ce.git"
export CI_BUILD_TAG="1.0.0"
export CI_BUILD_NAME="spec:other"
export CI_BUILD_STAGE="test"
@@ -73,9 +84,9 @@ export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce"
export CI_PROJECT_NAME="gitlab-ce"
export CI_PROJECT_NAMESPACE="gitlab-org"
export CI_PROJECT_PATH="gitlab-org/gitlab-ce"
-export CI_PROJECT_URL="https://gitlab.com/gitlab-org/gitlab-ce"
-export CI_REGISTRY="registry.gitlab.com"
-export CI_REGISTRY_IMAGE="registry.gitlab.com/gitlab-org/gitlab-ce"
+export CI_PROJECT_URL="https://example.com/gitlab-org/gitlab-ce"
+export CI_REGISTRY="registry.example.com"
+export CI_REGISTRY_IMAGE="registry.example.com/gitlab-org/gitlab-ce"
export CI_RUNNER_ID="10"
export CI_RUNNER_DESCRIPTION="my runner"
export CI_RUNNER_TAGS="docker, linux"
@@ -84,32 +95,80 @@ export CI_SERVER_NAME="GitLab"
export CI_SERVER_REVISION="70606bf"
export CI_SERVER_VERSION="8.9.0"
export GITLAB_USER_ID="42"
-export GITLAB_USER_EMAIL="alexzander@sporer.com"
+export GITLAB_USER_EMAIL="user@example.com"
```
-### YAML-defined variables
-**This feature requires GitLab Runner 0.5.0 or higher and GitLab CI 7.14 or higher **
+## `.gitlab-ci.yaml` defined variables
+
+>**Note:**
+This feature requires GitLab Runner 0.5.0 or higher and GitLab CI 7.14 or higher.
-GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build environment.
-The variables are stored in repository and are meant to store non-sensitive project configuration, ie. RAILS_ENV or DATABASE_URL.
+GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in the
+build environment. The variables are hence saved in the repository, and they
+are meant to store non-sensitive project configuration, e.g., `RAILS_ENV` or
+`DATABASE_URL`.
+
+For example, if you set the variable below globally (not inside a job), it will
+be used in all executed commands and scripts:
```yaml
variables:
DATABASE_URL: "postgres://postgres@postgres/my_database"
```
-These variables can be later used in all executed commands and scripts.
+The YAML-defined variables are also set to all created
+[service containers](../docker/using_docker_images.md), thus allowing to fine
+tune them.
+
+Variables can be defined at a global level, but also at a job level. To turn off
+global defined variables in your job, define an empty array:
+
+```yaml
+job_name:
+ variables: []
+```
+
+## Secret variables
-The YAML-defined variables are also set to all created service containers, thus allowing to fine tune them.
+>**Notes:**
+- This feature requires GitLab Runner 0.4.0 or higher.
+- Be aware that secret variables are not masked, and their values can be shown
+ in the build logs if explicitly asked to do so. If your project is public or
+ internal, you can set the pipelines private from your project's Pipelines
+ settings. Follow the discussion in issue [#13784][ce-13784] for masking the
+ secret variables.
-Variables can be defined at a global level, but also at a job level.
+GitLab CI allows you to define per-project **secret variables** that are set in
+the build environment. The secret variables are stored out of the repository
+(`.gitlab-ci.yml`) and are securely passed to GitLab Runner making them
+available in the build environment. It's the recommended method to use for
+storing things like passwords, secret keys and credentials.
-More information about Docker integration can be found in [Using Docker Images](../docker/using_docker_images.md).
+Secret variables can be added by going to your project's
+**Settings ➔ Variables ➔ Add variable**.
-#### Debug tracing
+Once you set them, they will be available for all subsequent builds.
+## Deployment variables
+
+>**Note:**
+This feature requires GitLab CI 8.15 or higher.
+
+[Project services](../../project_services/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).
+
+## Debug tracing
+
+> Introduced in GitLab Runner 1.7.
+>
> **WARNING:** Enabling debug tracing can have severe security implications. The
- output **will** contain the content of all your secure variables and any other
+ output **will** contain the content of all your secret variables and any other
secrets! The output **will** be uploaded to the GitLab server and made visible
in build traces!
@@ -124,55 +183,162 @@ trace, resulting in a verbose build trace listing all commands that were run,
variables that were set, etc.
Before enabling this, you should ensure builds are visible to
-[team members only](../../../user/permissions.md#project-features). You should
-also [erase](../pipelines.md#seeing-build-traces) all generated build traces
+[team members only](../../user/permissions.md#project-features). You should
+also [erase](../pipelines.md#seeing-build-status) all generated build traces
before making them visible again.
To enable debug traces, set the `CI_DEBUG_TRACE` variable to `true`:
```yaml
-job1:
+job_name:
variables:
CI_DEBUG_TRACE: "true"
```
-The [example project](https://gitlab.com/gitlab-examples/ci-debug-trace)
-demonstrates a working configuration, including build trace examples.
-
-### User-defined variables (Secure Variables)
-**This feature requires GitLab Runner 0.4.0 or higher**
+Example truncated output with debug trace set to true:
-GitLab CI allows you to define per-project **Secure Variables** that are set in
-the build environment.
-The secure variables are stored out of the repository (the `.gitlab-ci.yml`).
-The variables are securely passed to GitLab Runner and are available in the
-build environment.
-It's desired method to use them for storing passwords, secret keys or whatever
-you want.
+```bash
+...
+
+export CI_SERVER_TLS_CA_FILE="/builds/gitlab-examples/ci-debug-trace.tmp/CI_SERVER_TLS_CA_FILE"
+if [[ -d "/builds/gitlab-examples/ci-debug-trace/.git" ]]; then
+ echo $'\''\x1b[32;1mFetching changes...\x1b[0;m'\''
+ $'\''cd'\'' "/builds/gitlab-examples/ci-debug-trace"
+ $'\''git'\'' "config" "fetch.recurseSubmodules" "false"
+ $'\''rm'\'' "-f" ".git/index.lock"
+ $'\''git'\'' "clean" "-ffdx"
+ $'\''git'\'' "reset" "--hard"
+ $'\''git'\'' "remote" "set-url" "origin" "https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@example.com/gitlab-examples/ci-debug-trace.git"
+ $'\''git'\'' "fetch" "origin" "--prune" "+refs/heads/*:refs/remotes/origin/*" "+refs/tags/*:refs/tags/*"
+else
+ $'\''mkdir'\'' "-p" "/builds/gitlab-examples/ci-debug-trace.tmp/git-template"
+ $'\''rm'\'' "-r" "-f" "/builds/gitlab-examples/ci-debug-trace"
+ $'\''git'\'' "config" "-f" "/builds/gitlab-examples/ci-debug-trace.tmp/git-template/config" "fetch.recurseSubmodules" "false"
+ echo $'\''\x1b[32;1mCloning repository...\x1b[0;m'\''
+ $'\''git'\'' "clone" "--no-checkout" "https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@example.com/gitlab-examples/ci-debug-trace.git" "/builds/gitlab-examples/ci-debug-trace" "--template" "/builds/gitlab-examples/ci-debug-trace.tmp/git-template"
+ $'\''cd'\'' "/builds/gitlab-examples/ci-debug-trace"
+fi
+echo $'\''\x1b[32;1mChecking out dd648b2e as master...\x1b[0;m'\''
+$'\''git'\'' "checkout" "-f" "-q" "dd648b2e48ce6518303b0bb580b2ee32fadaf045"
+'
++++ hostname
+++ echo 'Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-machine-1480971377-317a7d0f-digital-ocean-4gb...'
+Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-machine-1480971377-317a7d0f-digital-ocean-4gb...
+++ export CI=true
+++ CI=true
+++ export CI_DEBUG_TRACE=false
+++ CI_DEBUG_TRACE=false
+++ export CI_BUILD_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ CI_BUILD_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ export CI_BUILD_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ CI_BUILD_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ export CI_BUILD_REF_NAME=master
+++ CI_BUILD_REF_NAME=master
+++ export CI_BUILD_ID=7046507
+++ CI_BUILD_ID=7046507
+++ export CI_BUILD_REPO=https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@example.com/gitlab-examples/ci-debug-trace.git
+++ CI_BUILD_REPO=https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@example.com/gitlab-examples/ci-debug-trace.git
+++ export CI_BUILD_TOKEN=xxxxxxxxxxxxxxxxxxxx
+++ CI_BUILD_TOKEN=xxxxxxxxxxxxxxxxxxxx
+++ export CI_PROJECT_ID=1796893
+++ CI_PROJECT_ID=1796893
+++ export CI_PROJECT_DIR=/builds/gitlab-examples/ci-debug-trace
+++ CI_PROJECT_DIR=/builds/gitlab-examples/ci-debug-trace
+++ export CI_SERVER=yes
+++ CI_SERVER=yes
+++ export 'CI_SERVER_NAME=GitLab CI'
+++ CI_SERVER_NAME='GitLab CI'
+++ export CI_SERVER_VERSION=
+++ CI_SERVER_VERSION=
+++ export CI_SERVER_REVISION=
+++ CI_SERVER_REVISION=
+++ export GITLAB_CI=true
+++ GITLAB_CI=true
+++ export CI=true
+++ CI=true
+++ export GITLAB_CI=true
+++ GITLAB_CI=true
+++ export CI_BUILD_ID=7046507
+++ CI_BUILD_ID=7046507
+++ export CI_BUILD_TOKEN=xxxxxxxxxxxxxxxxxxxx
+++ CI_BUILD_TOKEN=xxxxxxxxxxxxxxxxxxxx
+++ export CI_BUILD_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ CI_BUILD_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ export CI_BUILD_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ CI_BUILD_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ export CI_BUILD_REF_NAME=master
+++ CI_BUILD_REF_NAME=master
+++ export CI_BUILD_NAME=debug_trace
+++ CI_BUILD_NAME=debug_trace
+++ export CI_BUILD_STAGE=test
+++ CI_BUILD_STAGE=test
+++ export CI_SERVER_NAME=GitLab
+++ CI_SERVER_NAME=GitLab
+++ export CI_SERVER_VERSION=8.14.3-ee
+++ CI_SERVER_VERSION=8.14.3-ee
+++ export CI_SERVER_REVISION=82823
+++ CI_SERVER_REVISION=82823
+++ export CI_PROJECT_ID=17893
+++ CI_PROJECT_ID=17893
+++ export CI_PROJECT_NAME=ci-debug-trace
+++ CI_PROJECT_NAME=ci-debug-trace
+++ export CI_PROJECT_PATH=gitlab-examples/ci-debug-trace
+++ CI_PROJECT_PATH=gitlab-examples/ci-debug-trace
+++ export CI_PROJECT_NAMESPACE=gitlab-examples
+++ CI_PROJECT_NAMESPACE=gitlab-examples
+++ export CI_PROJECT_URL=https://example.com/gitlab-examples/ci-debug-trace
+++ CI_PROJECT_URL=https://example.com/gitlab-examples/ci-debug-trace
+++ export CI_PIPELINE_ID=52666
+++ CI_PIPELINE_ID=52666
+++ export CI_RUNNER_ID=1337
+++ CI_RUNNER_ID=1337
+++ export CI_RUNNER_DESCRIPTION=shared-runners-manager-1.example.com
+++ CI_RUNNER_DESCRIPTION=shared-runners-manager-1.example.com
+++ export 'CI_RUNNER_TAGS=shared, docker, linux, ruby, mysql, postgres, mongo, git-annex'
+++ CI_RUNNER_TAGS='shared, docker, linux, ruby, mysql, postgres, mongo, git-annex'
+++ export CI_REGISTRY=registry.example.com
+++ CI_REGISTRY=registry.example.com
+++ export CI_DEBUG_TRACE=true
+++ CI_DEBUG_TRACE=true
+++ export GITLAB_USER_ID=42
+++ GITLAB_USER_ID=42
+++ export GITLAB_USER_EMAIL=user@example.com
+++ GITLAB_USER_EMAIL=axilleas@axilleas.me
+++ export VERY_SECURE_VARIABLE=imaverysecurevariable
+++ VERY_SECURE_VARIABLE=imaverysecurevariable
+++ mkdir -p /builds/gitlab-examples/ci-debug-trace.tmp
+++ echo -n '-----BEGIN CERTIFICATE-----
+MIIFQzCCBCugAwIBAgIRAL/ElDjuf15xwja1ZnCocWAwDQYJKoZIhvcNAQELBQAw'
+
+...
+```
-**The value of the variable can be shown in build log if explicitly asked to do so.**
-If your project is public or internal you can make the builds private.
+## Using the CI variables in your job scripts
-Secure Variables can added by going to `Project > Variables > Add Variable`.
+All variables are set as environment variables in the build environment, and
+they are accessible with normal methods that are used to access such variables.
+In most cases `bash` or `sh` is used to execute the build script.
-They will be available for all subsequent builds.
+To access the variables (predefined and user-defined) in a `bash`/`sh` environment,
+prefix the variable name with the dollar sign (`$`):
-### Use variables
-The variables are set as environment variables in build environment and are accessible with normal methods that are used to access such variables.
-In most cases the **bash** is used to execute build script.
-To access variables (predefined and user-defined) in bash environment, prefix the variable name with `$`:
```
job_name:
script:
- echo $CI_BUILD_ID
```
-You can also list all environment variables with `export` command,
-but be aware that this will also expose value of all **Secure Variables** in build log:
+You can also list all environment variables with the `export` command,
+but be aware that this will also expose the values of all the secret variables
+you set, in the build log:
+
```
job_name:
script:
- export
```
+[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784
+[runner]: https://docs.gitlab.com/runner/
[triggered]: ../triggers/README.md
+[triggers]: ../triggers/README.md#pass-build-variables-to-a-trigger
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 5c0e1c44e3f..a62193700fc 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -406,14 +406,20 @@ except master.
### job variables
It is possible to define build variables using a `variables` keyword on a job
-level. It works basically the same way as its global-level equivalent but
-allows you to define job-specific build variables.
+level. It works basically the same way as its [global-level equivalent](#variables)
+but allows you to define job-specific build variables.
When the `variables` keyword is used on a job level, it overrides global YAML
-build variables and predefined variables.
+build variables and predefined variables. To turn off global defined variables
+in your job, define an empty array:
-Build variables priority is defined in
-[variables documentation](../variables/README.md).
+```yaml
+job_name:
+ variables: []
+```
+
+Build variables priority is defined in the
+[variables documentation][variables].
### tags
@@ -541,6 +547,8 @@ same manual action multiple times.
An example usage of manual actions is deployment to production.
+Read more at the [environments documentation][env-manual].
+
### environment
> Introduced in GitLab 8.9.
@@ -552,28 +560,14 @@ An example usage of manual actions is deployment to production.
If `environment` is specified and no environment under that name exists, a new
one will be created automatically.
-The `environment` name can contain:
-
-- letters
-- digits
-- spaces
-- `-`
-- `_`
-- `/`
-- `$`
-- `{`
-- `}`
-
-Common names are `qa`, `staging`, and `production`, but you can use whatever
-name works with your workflow.
-
In its simplest form, the `environment` keyword can be defined like:
```
deploy to production:
stage: deploy
script: git push production HEAD:master
- environment: production
+ environment:
+ name: production
```
In the above example, the `deploy to production` job will be marked as doing a
@@ -588,6 +582,21 @@ Before GitLab 8.11, the name of an environment could be defined as a string like
`environment: production`. The recommended way now is to define it under the
`name` keyword.
+The `environment` name can contain:
+
+- letters
+- digits
+- spaces
+- `-`
+- `_`
+- `/`
+- `$`
+- `{`
+- `}`
+
+Common names are `qa`, `staging`, and `production`, but you can use whatever
+name works with your workflow.
+
Instead of defining the name of the environment right after the `environment`
keyword, it is also possible to define it as a separate value. For that, use
the `name` keyword under `environment`:
@@ -626,7 +635,12 @@ deploy to production:
#### environment:on_stop
-> [Introduced][ce-6669] in GitLab 8.13.
+>
+**Notes:**
+- [Introduced][ce-6669] in GitLab 8.13.
+- Starting with GitLab 8.14, when you have an environment that has a stop action
+ defined, GitLab will automatically trigger a stop action when the associated
+ branch is deleted.
Closing (stoping) environments can be achieved with the `on_stop` keyword defined under
`environment`. It declares a different job that runs in order to close
@@ -676,6 +690,7 @@ The `stop_review_app` job is **required** to have the following keywords defined
#### dynamic environments
> [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6.
+ `$CI_ENVIRONMENT_SLUG` was [introduced][ce-7983] in GitLab 8.15
`environment` can also represent a configuration hash with `name` and `url`.
These parameters can use any of the defined [CI variables](#variables)
@@ -688,15 +703,17 @@ deploy as review app:
stage: deploy
script: make deploy
environment:
- name: review-apps/$CI_BUILD_REF_NAME
- url: https://$CI_BUILD_REF_NAME.review.example.com/
+ name: review/$CI_BUILD_REF_NAME
+ url: https://$CI_ENVIRONMENT_SLUG.example.com/
```
The `deploy as review app` job will be marked as deployment to dynamically
-create the `review-apps/$CI_BUILD_REF_NAME` environment, which `$CI_BUILD_REF_NAME`
-is an [environment variable][variables] set by the Runner. If for example the
-`deploy as review app` job was run in a branch named `pow`, this environment
-should be accessible under `https://pow.review.example.com/`.
+create the `review/$CI_BUILD_REF_NAME` environment, where `$CI_BUILD_REF_NAME`
+is an [environment variable][variables] set by the Runner. The
+`$CI_ENVIRONMENT_SLUG` variable is based on the environment name, but suitable
+for inclusion in URLs. In this case, if the `deploy as review app` job was run
+in a branch named `pow`, this environment would be accessible with an URL like
+`https://review-pow-aaaaaa.example.com/`.
This of course implies that the underlying server which hosts the application
is properly configured.
@@ -745,6 +762,15 @@ artifacts:
- binaries/
```
+To disable artifact passing, define the job with empty [dependencies](#dependencies):
+
+```yaml
+job:
+ stage: build
+ script: make build
+ dependencies: []
+```
+
You may want to create artifacts only for tagged releases to avoid filling the
build server storage with temporary build artifacts.
@@ -1210,8 +1236,10 @@ capitalization, the commit will be created but the builds will be skipped.
Visit the [examples README][examples] to see a list of examples using GitLab
CI with various languages.
+[env-manual]: ../environments.md#manually-deploying-to-environments
[examples]: ../examples/README.md
[ce-6323]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6323
[environment]: ../environments.md
[ce-6669]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6669
[variables]: ../variables/README.md
+[ce-7983]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7983
diff --git a/doc/customization/branded_login_page/appearance.png b/doc/customization/branded_login_page/appearance.png
index 023dc5599b4..31ea4559d37 100644
--- a/doc/customization/branded_login_page/appearance.png
+++ b/doc/customization/branded_login_page/appearance.png
Binary files differ
diff --git a/doc/customization/branded_login_page/custom_sign_in.png b/doc/customization/branded_login_page/custom_sign_in.png
index 7d99e0a2b3b..c0888fe1f18 100644
--- a/doc/customization/branded_login_page/custom_sign_in.png
+++ b/doc/customization/branded_login_page/custom_sign_in.png
Binary files differ
diff --git a/doc/customization/branded_login_page/default_login_page.png b/doc/customization/branded_login_page/default_login_page.png
index 0cfa9da202e..9b1233cef45 100644
--- a/doc/customization/branded_login_page/default_login_page.png
+++ b/doc/customization/branded_login_page/default_login_page.png
Binary files differ
diff --git a/doc/development/README.md b/doc/development/README.md
index bf1f054b7d5..6f2ca7b8590 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -14,7 +14,7 @@
contributing to documentation.
- [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations
- [Testing standards and style guidelines](testing.md)
-- [UI guide](ui_guide.md) for building GitLab with existing CSS styles and elements
+- [UX guide](ux_guide/index.md) for building GitLab with existing CSS styles and elements
- [Frontend guidelines](frontend.md)
- [SQL guidelines](sql.md) for working with SQL queries
- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
@@ -22,6 +22,7 @@
## Process
- [Generate a changelog entry with `bin/changelog`](changelog.md)
+- [Limit conflicts with EE when developing on CE](limit_ee_conflicts.md)
- [Code review guidelines](code_review.md) for reviewing code and having code reviewed.
- [Merge request performance guidelines](merge_request_performance_guidelines.md)
for ensuring merge requests do not negatively impact GitLab performance
@@ -37,6 +38,7 @@
- [Rake tasks](rake_tasks.md) for development
- [Shell commands](shell_commands.md) in the GitLab codebase
- [Sidekiq debugging](sidekiq_debugging.md)
+- [Object state models](object_state_models.md)
## Databases
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index c5c23b5c0b8..1ef34c79971 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -70,10 +70,36 @@ experience, refactors the existing code). Then:
- After a round of line notes, it can be helpful to post a summary note such as
"LGTM :thumbsup:", or "Just a couple things to address."
- Avoid accepting a merge request before the build succeeds. Of course, "Merge
- When Build Succeeds" (MWBS) is fine.
-- If you set the MR to "Merge When Build Succeeds", you should take over
+ When Pipeline Succeeds" (MWPS) is fine.
+- If you set the MR to "Merge When Pipeline Succeeds", you should take over
subsequent revisions for anything that would be spotted after that.
+## The right balance
+
+One of the most difficult things during code review is finding the right
+balance in how deep the reviewer can interfere with the code created by a
+reviewee.
+
+- Learning how to find the right balance takes time; that is why we have
+ minibosses that become merge request endbosses after some time spent on
+ reviewing merge requests.
+- Finding bugs and improving code style is important, but thinking about good
+ design is important as well. Building abstractions and good design is what
+ makes it possible to hide complexity and makes future changes easier.
+- Asking the reviewee to change the design sometimes means the complete rewrite
+ of the contributed code. It's usually a good idea to ask another merge
+ request endboss before doing it, but have the courage to do it when you
+ believe it is important.
+- There is a difference in doing things right and doing things right now.
+ Ideally, we should do the former, but in the real world we need the latter as
+ well. A good example is a security fix which should be released as soon as
+ possible. Asking the reviewee to do the major refactoring in the merge
+ request that is an urgent fix should be avoided.
+- Doing things well today is usually better than doing something perfectly
+ tomorrow. Shipping a kludge today is usually worse than doing something well
+ tomorrow. When you are not able to find the right balance, ask other people
+ about their opinion.
+
## Credits
Largely based on the [thoughtbot code review guide].
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index b137e6ae82e..fc948a7a116 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -113,6 +113,77 @@ merge request.
add an alternative text: `[identifier]: https://example.com "Alternative text"`
that appears when hovering your mouse on a link
+### Linking to inline docs
+
+Sometimes it's needed to link to the built-in documentation that GitLab provides
+under `/help`. This is normally done in files inside the `app/views/` directory
+with the help of the `help_page_path` helper method.
+
+In its simplest form, the HAML code to generate a link to the `/help` page is:
+
+```haml
+= link_to 'Help page', help_page_path('user/permissions')
+```
+
+The `help_page_path` contains the path to the document you want to link to with
+the following conventions:
+
+- it is relative to the `doc/` directory in the GitLab repository
+- the `.md` extension must be omitted
+- it must not end with a slash (`/`)
+
+Below are some special cases where should be used depending on the context.
+You can combine one or more of the following:
+
+1. **Linking to an anchor link.** Use `anchor` as part of the `help_page_path`
+ method:
+
+ ```haml
+ = link_to 'Help page', help_page_path('user/permissions', anchor: 'anchor-link')
+ ```
+
+1. **Opening links in a new tab.** This should be the default behavior:
+
+ ```haml
+ = link_to 'Help page', help_page_path('user/permissions'), target: '_blank'
+ ```
+
+1. **Linking to a circle icon.** Usually used in settings where a long
+ description cannot be used, like near checkboxes. You can basically use
+ any font awesome icon, but prefer the `question-circle`:
+
+ ```haml
+ = link_to icon('question-circle'), help_page_path('user/permissions')
+ ```
+
+1. **Using a button link.** Useful in places where text would be out of context
+ with the rest of the page layout:
+
+ ```haml
+ = link_to 'Help page', help_page_path('user/permissions'), class: 'btn btn-info'
+ ```
+
+1. **Underlining a link.**
+
+ ```haml
+ = link_to 'Help page', help_page_path('user/permissions'), class: 'underlined-link'
+ ```
+
+1. **Using links inline of some text.**
+
+ ```haml
+ Description to #{link_to 'Help page', help_page_path('user/permissions')}.
+ ```
+
+1. **Adding a period at the end of the sentence.** Useful when you don't want
+ the period to be part of the link:
+
+ ```haml
+ = succeed '.' do
+ Learn more in the
+ = link_to 'Help page', help_page_path('user/permissions')
+ ```
+
## Images
- Place images in a separate directory named `img/` in the same directory where
diff --git a/doc/development/frontend.md b/doc/development/frontend.md
index ec8f2d6531c..9e782ab977f 100644
--- a/doc/development/frontend.md
+++ b/doc/development/frontend.md
@@ -205,6 +205,57 @@ command line.
Please note: Not all of the frontend fixtures are generated. Some are still static
files. These will not be touched by `rake teaspoon:fixtures`.
+## Design Patterns
+
+### Singletons
+
+When exactly one object is needed for a given task, prefer to define it as a
+`class` rather than as an object literal. Prefer also to explicitly restrict
+instantiation, unless flexibility is important (e.g. for testing).
+
+```
+// bad
+
+gl.MyThing = {
+ prop1: 'hello',
+ method1: () => {}
+};
+
+// good
+
+class MyThing {
+ constructor() {
+ this.prop1 = 'hello';
+ }
+ method1() {}
+}
+
+gl.MyThing = new MyThing();
+
+// best
+
+let singleton;
+
+class MyThing {
+ constructor() {
+ if (!singleton) {
+ singleton = this;
+ singleton.init();
+ }
+ return singleton;
+ }
+
+ init() {
+ this.prop1 = 'hello';
+ }
+
+ method1() {}
+}
+
+gl.MyThing = MyThing;
+
+```
+
## Supported browsers
For our currently-supported browsers, see our [requirements][requirements].
diff --git a/doc/development/gitlab_architecture_diagram.png b/doc/development/gitlab_architecture_diagram.png
index 80e975718e0..cda5ce254ce 100644
--- a/doc/development/gitlab_architecture_diagram.png
+++ b/doc/development/gitlab_architecture_diagram.png
Binary files differ
diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md
index b25ce79e89f..0f78e8238af 100644
--- a/doc/development/gotchas.md
+++ b/doc/development/gotchas.md
@@ -32,71 +32,112 @@ spec/models/user_spec.rb|6 error| Failure/Error: u = described_class.new NoMeth
Except for the top-level `describe` block, always provide a String argument to
`describe`.
-## Don't `rescue Exception`
+## Don't assert against the absolute value of a sequence-generated attribute
-See ["Why is it bad style to `rescue Exception => e` in Ruby?"][Exception].
+Consider the following factory:
-_**Note:** This rule is [enforced automatically by
-Rubocop](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/.rubocop.yml#L911-914)._
+```ruby
+FactoryGirl.define do
+ factory :label do
+ sequence(:title) { |n| "label#{n}" }
+ end
+end
+```
-[Exception]: http://stackoverflow.com/q/10048173/223897
+Consider the following API spec:
-## Don't use inline JavaScript in views
+```ruby
+require 'rails_helper'
-Using the inline `:javascript` Haml filters comes with a
-performance overhead. Using inline JavaScript is not a good way to structure your code and should be avoided.
+describe API::Labels do
+ it 'creates a first label' do
+ create(:label)
-_**Note:** We've [removed these two filters](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/initializers/hamlit.rb)
-in an initializer._
-
-### Further reading
+ get api("/projects/#{project.id}/labels", user)
-- Stack Overflow: [Why you should not write inline JavaScript](http://programmers.stackexchange.com/questions/86589/why-should-i-avoid-inline-scripting)
+ expect(response).to have_http_status(200)
+ expect(json_response.first['name']).to eq('label1')
+ end
-## ID-based CSS selectors need to be a bit more specific
+ it 'creates a second label' do
+ create(:label)
-Normally, because HTML `id` attributes need to be unique to the page, it's
-perfectly fine to write some JavaScript like the following:
+ get api("/projects/#{project.id}/labels", user)
-```javascript
-$('#js-my-selector').hide();
+ expect(response).to have_http_status(200)
+ expect(json_response.first['name']).to eq('label1')
+ end
+end
```
-However, there's a feature of GitLab's Markdown processing that [automatically
-adds anchors to header elements][ToC Processing], with the `id` attribute being
-automatically generated based on the content of the header.
-
-Unfortunately, this feature makes it possible for user-generated content to
-create a header element with the same `id` attribute we're using in our
-selector, potentially breaking the JavaScript behavior. A user could break the
-above example with the following Markdown:
+When run, this spec doesn't do what we might expect:
-```markdown
-## JS My Selector
-```
+```sh
+1) API::API reproduce sequence issue creates a second label
+ Failure/Error: expect(json_response.first['name']).to eq('label1')
-Which gets converted to the following HTML:
+ expected: "label1"
+ got: "label2"
-```html
-<h2>
- <a id="js-my-selector" class="anchor" href="#js-my-selector" aria-hidden="true"></a>
- JS My Selector
-</h2>
+ (compared using ==)
```
-[ToC Processing]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/lib/banzai/filter/table_of_contents_filter.rb#L31-37
+That's because FactoryGirl sequences are not reseted for each example.
+
+Please remember that sequence-generated values exist only to avoid having to
+explicitly set attributes that have a uniqueness constraint when using a factory.
### Solution
-The current recommended fix for this is to make our selectors slightly more
-specific:
+If you assert against a sequence-generated attribute's value, you should set it
+explicitly. Also, the value you set shouldn't match the sequence pattern.
+
+For instance, using our `:label` factory, writing `create(:label, title: 'foo')`
+is ok, but `create(:label, title: 'label1')` is not.
+
+Following is the fixed API spec:
-```javascript
-$('div#js-my-selector').hide();
+```ruby
+require 'rails_helper'
+
+describe API::Labels do
+ it 'creates a first label' do
+ create(:label, title: 'foo')
+
+ get api("/projects/#{project.id}/labels", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.first['name']).to eq('foo')
+ end
+
+ it 'creates a second label' do
+ create(:label, title: 'bar')
+
+ get api("/projects/#{project.id}/labels", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.first['name']).to eq('bar')
+ end
+end
```
+## Don't `rescue Exception`
+
+See ["Why is it bad style to `rescue Exception => e` in Ruby?"][Exception].
+
+_**Note:** This rule is [enforced automatically by
+Rubocop](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/.rubocop.yml#L911-914)._
+
+[Exception]: http://stackoverflow.com/q/10048173/223897
+
+## Don't use inline JavaScript in views
+
+Using the inline `:javascript` Haml filters comes with a
+performance overhead. Using inline JavaScript is not a good way to structure your code and should be avoided.
+
+_**Note:** We've [removed these two filters](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/initializers/hamlit.rb)
+in an initializer._
+
### Further reading
-- Issue: [Merge request ToC anchor conflicts with tabs](https://gitlab.com/gitlab-org/gitlab-ce/issues/3908)
-- Merge Request: [Make tab target selectors less naive](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2023)
-- Merge Request: [Make cross-project reference's clipboard target less naive](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2024)
+- Stack Overflow: [Why you should not write inline JavaScript](http://programmers.stackexchange.com/questions/86589/why-should-i-avoid-inline-scripting)
diff --git a/doc/development/img/state-model-issue.png b/doc/development/img/state-model-issue.png
new file mode 100644
index 00000000000..ee33b6886c6
--- /dev/null
+++ b/doc/development/img/state-model-issue.png
Binary files differ
diff --git a/doc/development/img/state-model-legend.png b/doc/development/img/state-model-legend.png
new file mode 100644
index 00000000000..1c121f2588c
--- /dev/null
+++ b/doc/development/img/state-model-legend.png
Binary files differ
diff --git a/doc/development/img/state-model-merge-request.png b/doc/development/img/state-model-merge-request.png
new file mode 100644
index 00000000000..e00da10cac2
--- /dev/null
+++ b/doc/development/img/state-model-merge-request.png
Binary files differ
diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md
index 105e2f1242a..b8669964c84 100644
--- a/doc/development/instrumentation.md
+++ b/doc/development/instrumentation.md
@@ -24,7 +24,7 @@ namespace you can use the `configure` class method. This method simply yields
the supplied block while passing `Gitlab::Metrics::Instrumentation` as its
argument. An example:
-```
+```ruby
Gitlab::Metrics::Instrumentation.configure do |conf|
conf.instrument_method(Foo, :bar)
conf.instrument_method(Foo, :baz)
@@ -41,7 +41,7 @@ Method instrumentation should be added in the initializer
Instrumenting a single method:
-```
+```ruby
Gitlab::Metrics::Instrumentation.configure do |conf|
conf.instrument_method(User, :find_by)
end
@@ -49,7 +49,7 @@ end
Instrumenting an entire class hierarchy:
-```
+```ruby
Gitlab::Metrics::Instrumentation.configure do |conf|
conf.instrument_class_hierarchy(ActiveRecord::Base)
end
@@ -57,7 +57,7 @@ end
Instrumenting all public class methods:
-```
+```ruby
Gitlab::Metrics::Instrumentation.configure do |conf|
conf.instrument_methods(User)
end
@@ -68,7 +68,7 @@ end
The easiest way to check if a method has been instrumented is to check its
source location. For example:
-```
+```ruby
method = Rugged::TagCollection.instance_method(:[])
method.source_location
diff --git a/doc/development/limit_ee_conflicts.md b/doc/development/limit_ee_conflicts.md
new file mode 100644
index 00000000000..568dedf1669
--- /dev/null
+++ b/doc/development/limit_ee_conflicts.md
@@ -0,0 +1,325 @@
+# Limit conflicts with EE when developing on CE
+
+This guide contains best-practices for avoiding conflicts between CE and EE.
+
+## Context
+
+Usually, GitLab Community Edition is merged into the Enterprise Edition once a
+week. During these merges, it's very common to get conflicts when some changes
+in CE do not apply cleanly to EE.
+
+There are a few things that can help you as a developer to:
+
+- know when your merge request to CE will conflict when merged to EE
+- avoid such conflicts in the first place
+- ease future conflict resolutions if conflict is inevitable
+
+## Check the `rake ee_compat_check` in your merge requests
+
+For each commit (except on `master`), the `rake ee_compat_check` CI job tries to
+detect if the current branch's changes will conflict during the CE->EE merge.
+
+The job reports what files are conflicting and how to setup a merge request
+against EE. Here is roughly how it works:
+
+1. Generates the diff between your branch and current CE `master`
+1. Tries to apply it to current EE `master`
+1. If it applies cleanly, the job succeeds, otherwise...
+1. Detects a branch with the `-ee` suffix in EE
+1. If it exists, generate the diff between this branch and current EE `master`
+1. Tries to apply it to current EE `master`
+1. If it applies cleanly, the job succeeds
+
+In the case where the job fails, it means you should create a `<ce_branch>-ee`
+branch, push it to EE and open a merge request against EE `master`. At this
+point if you retry the failing job in your CE merge request, it should now pass.
+
+Notes:
+
+- This task is not a silver-bullet, its current goal is to bring awareness to
+ developers that their work needs to be ported to EE.
+- Community contributors shouldn't submit merge requests against EE, but
+ reviewers should take actions by either creating such EE merge request or
+ asking a GitLab developer to do it once the merge request is merged.
+- If you branch is more than 500 commits behind `master`, the job will fail and
+ you should rebase your branch upon latest `master`.
+
+## Possible type of conflicts
+
+### Controllers
+
+#### List or arrays are augmented in EE
+
+In controllers, the most common type of conflict is with `before_action` that
+has a list of actions in CE but EE adds some actions to that list.
+
+The same problem often occurs for `params.require` / `params.permit` calls.
+
+##### Mitigations
+
+Separate CE and EE actions/keywords. For instance for `params.require` in
+`ProjectsController`:
+
+```ruby
+def project_params
+ params.require(:project).permit(project_params_ce)
+ # On EE, this is always:
+ # params.require(:project).permit(project_params_ce << project_params_ee)
+end
+
+# Always returns an array of symbols, created however best fits the use case.
+# It _should_ be sorted alphabetically.
+def project_params_ce
+ %i[
+ description
+ name
+ path
+ ]
+end
+
+# (On EE)
+def project_params_ee
+ %i[
+ approvals_before_merge
+ approver_group_ids
+ approver_ids
+ ...
+ ]
+end
+```
+
+#### Additional condition(s) in EE
+
+For instance for LDAP:
+
+```diff
+ def destroy
+ @key = current_user.keys.find(params[:id])
+ - @key.destroy
+ + @key.destroy unless @key.is_a? LDAPKey
+
+ respond_to do |format|
+```
+
+Or for Geo:
+
+```diff
+def after_sign_out_path_for(resource)
+- current_application_settings.after_sign_out_path.presence || new_user_session_path
++ if Gitlab::Geo.secondary?
++ Gitlab::Geo.primary_node.oauth_logout_url(@geo_logout_state)
++ else
++ current_application_settings.after_sign_out_path.presence || new_user_session_path
++ end
+end
+```
+
+Or even for audit log:
+
+```diff
+def approve_access_request
+- Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute
++ member = Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute
++
++ log_audit_event(member, action: :create)
+
+ redirect_to polymorphic_url([membershipable, :members])
+end
+```
+
+### Views
+
+#### Additional view code in EE
+
+A block of code added in CE conflicts because there is already another block
+at the same place in EE
+
+##### Mitigations
+
+Blocks of code that are EE-specific should be moved to partials as much as
+possible to avoid conflicts with big chunks of HAML code that that are not fun
+to resolve when you add the indentation to the equation.
+
+For instance this kind of thing:
+
+```haml
+.form-group.detail-page-description
+ = form.label :description, 'Description', class: 'control-label'
+ .col-sm-10
+ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
+ = render 'projects/zen', f: form, attr: :description,
+ classes: 'note-textarea',
+ placeholder: "Write a comment or drag your files here...",
+ supports_slash_commands: !issuable.persisted?
+ = render 'projects/notes/hints', supports_slash_commands: !issuable.persisted?
+ .clearfix
+ .error-alert
+- if issuable.is_a?(Issue)
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = form.label :confidential do
+ = form.check_box :confidential
+ This issue is confidential and should only be visible to team members with at least Reporter access.
+- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
+ - has_due_date = issuable.has_attribute?(:due_date)
+ %hr
+ .row
+ %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
+ .form-group.issue-assignee
+ = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
+ .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+ .issuable-form-select-holder
+ - if issuable.assignee_id
+ = form.hidden_field :assignee_id
+ = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
+ placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
+ .form-group.issue-milestone
+ = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
+ .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+ .issuable-form-select-holder
+ = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
+ .form-group
+ - has_labels = @labels && @labels.any?
+ = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
+ = form.hidden_field :label_ids, multiple: true, value: ''
+ .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
+ .issuable-form-select-holder
+ = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label"
+ - if issuable.respond_to?(:weight)
+ - weight_options = Issue.weight_options
+ - weight_options.delete(Issue::WEIGHT_ALL)
+ - weight_options.delete(Issue::WEIGHT_ANY)
+ .form-group
+ = form.label :label_ids, class: "control-label #{"col-lg-4" if has_due_date}" do
+ Weight
+ .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+ .issuable-form-select-holder
+ - if issuable.weight
+ = form.hidden_field :weight
+ = dropdown_tag(issuable.weight || "Weight", options: { title: "Select weight", toggle_class: 'js-weight-select js-issuable-form-weight', dropdown_class: "dropdown-menu-selectable dropdown-menu-weight",
+ placeholder: "Search weight", data: { field_name: "#{issuable.class.model_name.param_key}[weight]" , default_label: "Weight" } }) do
+ %ul
+ - weight_options.each do |weight|
+ %li
+ %a{href: "#", data: { id: weight, none: weight === Issue::WEIGHT_NONE }, class: ("is-active" if issuable.weight == weight)}
+ = weight
+ - if has_due_date
+ .col-lg-6
+ .form-group
+ = form.label :due_date, "Due date", class: "control-label"
+ .col-sm-10
+ .issuable-form-select-holder
+ = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
+```
+
+could be simplified by using partials:
+
+```haml
+= render 'shared/issuable/form/description', issuable: issuable, form: form
+
+- if issuable.respond_to?(:confidential)
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = form.label :confidential do
+ = form.check_box :confidential
+ This issue is confidential and should only be visible to team members with at least Reporter access.
+
+= render 'shared/issuable/form/metadata', issuable: issuable, form: form
+```
+
+and then the `app/views/shared/issuable/form/_metadata.html.haml` could be as follows:
+
+```haml
+- issuable = local_assigns.fetch(:issuable)
+
+- return unless can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
+
+- has_due_date = issuable.has_attribute?(:due_date)
+- has_labels = @labels && @labels.any?
+- form = local_assigns.fetch(:form)
+
+%hr
+.row
+ %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
+ .form-group.issue-assignee
+ = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
+ .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+ .issuable-form-select-holder
+ - if issuable.assignee_id
+ = form.hidden_field :assignee_id
+ = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
+ placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
+ .form-group.issue-milestone
+ = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
+ .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+ .issuable-form-select-holder
+ = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
+ .form-group
+ - has_labels = @labels && @labels.any?
+ = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
+ = form.hidden_field :label_ids, multiple: true, value: ''
+ .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
+ .issuable-form-select-holder
+ = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label"
+
+ = render "shared/issuable/form/weight", issuable: issuable, form: form
+
+ - if has_due_date
+ .col-lg-6
+ .form-group
+ = form.label :due_date, "Due date", class: "control-label"
+ .col-sm-10
+ .issuable-form-select-holder
+ = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
+```
+
+and then the `app/views/shared/issuable/form/_weight.html.haml` could be as follows:
+
+```haml
+- issuable = local_assigns.fetch(:issuable)
+
+- return unless issuable.respond_to?(:weight)
+
+- has_due_date = issuable.has_attribute?(:due_date)
+- form = local_assigns.fetch(:form)
+
+.form-group
+ = form.label :label_ids, class: "control-label #{"col-lg-4" if has_due_date}" do
+ Weight
+ .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+ .issuable-form-select-holder
+ - if issuable.weight
+ = form.hidden_field :weight
+
+ = weight_dropdown_tag(issuable, toggle_class: 'js-issuable-form-weight') do
+ %ul
+ - Issue.weight_options.each do |weight|
+ %li
+ %a{ href: '#', data: { id: weight, none: weight === Issue::WEIGHT_NONE }, class: ("is-active" if issuable.weight == weight) }
+ = weight
+```
+
+Note:
+
+- The safeguards at the top allow to get rid of an unneccessary indentation level
+- Here we only moved the 'Weight' code to a partial since this is the only
+ EE-specific code in that view, so it's the most likely to conflict, but you
+ are encouraged to use partials even for code that's in CE to logically split
+ big views into several smaller files.
+
+#### Indentation issue
+
+Sometimes a code block is indented more or less in EE because there's an
+additional condition.
+
+##### Mitigations
+
+Blocks of code that are EE-specific should be moved to partials as much as
+possible to avoid conflicts with big chunks of HAML code that that are not fun
+to resolve when you add the indentation in the equation.
+
+---
+
+[Return to Development documentation](README.md)
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 61b0fbc89c9..fd8335d251e 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -60,7 +60,7 @@ migration was tested.
If you need to remove index, please add a condition like in following example:
-```
+```ruby
remove_index :namespaces, column: :name if index_exists?(:namespaces, :name)
```
@@ -75,7 +75,7 @@ need for downtime. To use this method you must disable transactions by calling
the method `disable_ddl_transaction!` in the body of your migration class like
so:
-```
+```ruby
class MyMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
@@ -96,7 +96,7 @@ the `up` and `down` methods in your migration class.
For example, to add the column `foo` to the `projects` table with a default
value of `10` you'd write the following:
-```
+```ruby
class MyMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
@@ -125,7 +125,7 @@ set the limit to 8-bytes. This will allow the column to hold a value up to
Rails migration example:
-```
+```ruby
add_column_with_default(:projects, :foo, :integer, default: 10, limit: 8)
# or
@@ -145,7 +145,7 @@ Please prefer Arel and plain SQL over usual ActiveRecord syntax. In case of usin
Example with Arel:
-```
+```ruby
users = Arel::Table.new(:users)
users.group(users[:user_id]).having(users[:id].count.gt(5))
@@ -154,7 +154,7 @@ users.group(users[:user_id]).having(users[:id].count.gt(5))
Example with plain SQL and `quote_string` helper:
-```
+```ruby
select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(id) > 1").each do |tag|
tag_name = quote_string(tag["name"])
duplicate_ids = select_all("SELECT id FROM tags WHERE name = '#{tag_name}'").map{|tag| tag["id"]}
diff --git a/doc/development/object_state_models.md b/doc/development/object_state_models.md
new file mode 100644
index 00000000000..623bbf143ef
--- /dev/null
+++ b/doc/development/object_state_models.md
@@ -0,0 +1,25 @@
+# Object state models
+
+## Diagrams
+
+[GitLab object state models](https://drive.google.com/drive/u/3/folders/0B5tDlHAM4iZINmpvYlJXcDVqMGc)
+
+---
+
+## Legend
+
+![legend](img/state-model-legend.png)
+
+---
+
+## Issue
+
+[`app/models/issue.rb`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/models/issue.rb)
+![issue](img/state-model-issue.png)
+
+---
+
+## Merge request
+
+[`app/models/merge_request.rb`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/models/merge_request.rb)
+![merge request](img/state-model-merge-request.png) \ No newline at end of file
diff --git a/doc/development/performance.md b/doc/development/performance.md
index 8337c2d9cb3..5c43ae7b79a 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -101,6 +101,116 @@ In short:
5. If you must write a benchmark use the benchmark-ips Gem instead of Ruby's
`Benchmark` module.
+## Profiling
+
+By collecting snapshots of process state at regular intervals, profiling allows
+you to see where time is spent in a process. The [StackProf](https://github.com/tmm1/stackprof)
+gem is included in GitLab's development environment, allowing you to investigate
+the behaviour of suspect code in detail.
+
+It's important to note that profiling an application *alters its performance*,
+and will generally be done *in an unrepresentative environment*. In particular,
+a method is not necessarily troublesome just because it is executed many times,
+or takes a long time to execute. Profiles are tools you can use to better
+understand what is happening in an application - using that information wisely
+is up to you!
+
+Keeping that in mind, to create a profile, identify (or create) a spec that
+exercises the troublesome code path, then run it using the `bin/rspec-stackprof`
+helper, e.g.:
+
+```
+$ LIMIT=10 bin/rspec-stackprof spec/policies/project_policy_spec.rb
+8/8 |====== 100 ======>| Time: 00:00:18
+
+Finished in 18.19 seconds (files took 4.8 seconds to load)
+8 examples, 0 failures
+
+==================================
+ Mode: wall(1000)
+ Samples: 17033 (5.59% miss rate)
+ GC: 1901 (11.16%)
+==================================
+ TOTAL (pct) SAMPLES (pct) FRAME
+ 6000 (35.2%) 2566 (15.1%) Sprockets::Cache::FileStore#get
+ 2018 (11.8%) 888 (5.2%) ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#exec_no_cache
+ 1338 (7.9%) 640 (3.8%) ActiveRecord::ConnectionAdapters::PostgreSQL::DatabaseStatements#execute
+ 3125 (18.3%) 394 (2.3%) Sprockets::Cache::FileStore#safe_open
+ 913 (5.4%) 301 (1.8%) ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#exec_cache
+ 288 (1.7%) 288 (1.7%) ActiveRecord::Attribute#initialize
+ 246 (1.4%) 246 (1.4%) Sprockets::Cache::FileStore#safe_stat
+ 295 (1.7%) 193 (1.1%) block (2 levels) in class_attribute
+ 187 (1.1%) 187 (1.1%) block (4 levels) in class_attribute
+```
+
+You can limit the specs that are run by passing any arguments `rspec` would
+normally take.
+
+The output is sorted by the `Samples` column by default. This is the number of
+samples taken where the method is the one currently being executed. The `Total`
+column shows the number of samples taken where the method, or any of the methods
+it calls, were being executed.
+
+To create a graphical view of the call stack:
+
+```shell
+$ stackprof tmp/project_policy_spec.rb.dump --graphviz > project_policy_spec.dot
+$ dot -Tsvg project_policy_spec.dot > project_policy_spec.svg
+```
+
+To load the profile in [kcachegrind](https://kcachegrind.github.io/):
+
+```
+$ stackprof tmp/project_policy_spec.dump --callgrind > project_policy_spec.callgrind
+$ kcachegrind project_policy_spec.callgrind # Linux
+$ qcachegrind project_policy_spec.callgrind # Mac
+```
+
+It may be useful to zoom in on a specific method, e.g.:
+
+```
+$ stackprof tmp/project_policy_spec.rb.dump --method warm_asset_cache
+TestEnv#warm_asset_cache (/Users/lupine/dev/gitlab.com/gitlab-org/gitlab-development-kit/gitlab/spec/support/test_env.rb:164)
+ samples: 0 self (0.0%) / 6288 total (36.9%)
+ callers:
+ 6288 ( 100.0%) block (2 levels) in <top (required)>
+ callees (6288 total):
+ 6288 ( 100.0%) Capybara::RackTest::Driver#visit
+ code:
+ | 164 | def warm_asset_cache
+ | 165 | return if warm_asset_cache?
+ | 166 | return unless defined?(Capybara)
+ | 167 |
+ 6288 (36.9%) | 168 | Capybara.current_session.driver.visit '/'
+ | 169 | end
+$ stackprof tmp/project_policy_spec.rb.dump --method BasePolicy#abilities
+BasePolicy#abilities (/Users/lupine/dev/gitlab.com/gitlab-org/gitlab-development-kit/gitlab/app/policies/base_policy.rb:79)
+ samples: 0 self (0.0%) / 50 total (0.3%)
+ callers:
+ 25 ( 50.0%) BasePolicy.abilities
+ 25 ( 50.0%) BasePolicy#collect_rules
+ callees (50 total):
+ 25 ( 50.0%) ProjectPolicy#rules
+ 25 ( 50.0%) BasePolicy#collect_rules
+ code:
+ | 79 | def abilities
+ | 80 | return RuleSet.empty if @user && @user.blocked?
+ | 81 | return anonymous_abilities if @user.nil?
+ 50 (0.3%) | 82 | collect_rules { rules }
+ | 83 | end
+```
+
+Since the profile includes the work done by the test suite as well as the
+application code, these profiles can be used to investigate slow tests as well.
+However, for smaller runs (like this example), this means that the cost of
+setting up the test suite will tend to dominate.
+
+It's also possible to modify the application code in-place to output profiles
+whenever a particular code path is triggered without going through the test
+suite first. See the
+[StackProf documentation](https://github.com/tmm1/stackprof/blob/master/README.md)
+for details.
+
## Importance of Changes
When working on performance improvements, it's important to always ask yourself
diff --git a/doc/development/shell_commands.md b/doc/development/shell_commands.md
index 65cdd74bdb6..73893f9dd46 100644
--- a/doc/development/shell_commands.md
+++ b/doc/development/shell_commands.md
@@ -129,7 +129,7 @@ Various methods for opening and reading files in Ruby can be used to read the
standard output of a process instead of a file. The following two commands do
roughly the same:
-```
+```ruby
`touch /tmp/pawned-by-backticks`
File.read('|touch /tmp/pawned-by-file-read')
```
@@ -142,7 +142,7 @@ attacker cannot control the start of the filename string you are opening. For
instance, the following is sufficient to protect against accidentally starting
a shell command with `|`:
-```
+```ruby
# we assume repo_path is not controlled by the attacker (user)
path = File.join(repo_path, user_input)
# path cannot start with '|' now.
@@ -160,7 +160,7 @@ Path traversal is a security where the program (GitLab) tries to restrict user
access to a certain directory on disk, but the user manages to open a file
outside that directory by taking advantage of the `../` path notation.
-```
+```ruby
# Suppose the user gave us a path and they are trying to trick us
user_input = '../other-repo.git/other-file'
@@ -177,7 +177,7 @@ File.open(full_path) do # Oops!
A good way to protect against this is to compare the full path with its
'absolute path' according to Ruby's `File.absolute_path`.
-```
+```ruby
full_path = File.join(repo_path, user_input)
if full_path != File.absolute_path(full_path)
raise "Invalid path: #{full_path.inspect}"
diff --git a/doc/development/testing.md b/doc/development/testing.md
index b0b26ccf57a..dbea6b9c9aa 100644
--- a/doc/development/testing.md
+++ b/doc/development/testing.md
@@ -63,12 +63,16 @@ the command line via `bundle exec teaspoon`, or via a web browser at
- Use `.method` to describe class methods and `#method` to describe instance
methods.
- Use `context` to test branching logic.
+- Use multi-line `do...end` blocks for `before` and `after`, even when it would
+ fit on a single line.
- Don't `describe` symbols (see [Gotchas](gotchas.md#dont-describe-symbols)).
+- Don't assert against the absolute value of a sequence-generated attribute (see [Gotchas](gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)).
- Don't supply the `:each` argument to hooks since it's the default.
- Prefer `not_to` to `to_not` (_this is enforced by Rubocop_).
- Try to match the ordering of tests to the ordering within the class.
- Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines
to separate phases.
+- Try to use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'`
[four-phase-test]: https://robots.thoughtbot.com/four-phase-test
diff --git a/doc/development/ux_guide/animation.md b/doc/development/ux_guide/animation.md
new file mode 100644
index 00000000000..daeb15460c2
--- /dev/null
+++ b/doc/development/ux_guide/animation.md
@@ -0,0 +1,42 @@
+# Animation
+
+Motion is a tool to help convey important relationships, changes or transitions between elements. It should be used sparingly and intentionally, highlighting the right elements at the right moment.
+
+## Timings
+
+The longer distance an object travel, the timing should be longer for the animation. However, when in doubt, we should avoid large, full screen animations.
+
+Subtle animations, or objects leaving the screen should take **100-200 milliseconds**. Objects entering the screen, or motion we want to use to direct user attention can take between **200-400 milliseconds**. We should avoid animations of longer than 400 milliseconds as they will make the experience appear sluggish. If a specific animation feels like it will need more than 400 milliseconds, revisit the animation to see if there is a simpler, easier, shorter animation to implement.
+
+## Easing
+
+Easing specifies the rate of change of a parameter over time (see [easings.net](http://easings.net/)). Adding an easing curve will make the motion feel more natural. Being consistent with the easing curves will make the whole experience feel more cohesive and connected.
+
+* When an object is entering the screen, or transforming the scale, position, or shape, use the **easeOutQuint** curve (`cubic-bezier(0.23, 1, 0.32, 1)`)
+* When an object is leaving the screen, or transforming the opacity or color, no easing curve is needed. It shouldn't _slow down_ as it is exiting the screen, as that draws attention on the leaving object, where we don't want it. Adding easing to opacity and color transitions will make the motion appear less smooth. Therefore, for these cases, motion should just be **linear**.
+
+## Types of animations
+
+### 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.
+
+View the [interactive example](http://codepen.io/awhildy/full/GNyEvM/) here.
+
+![Hover animation](img/animation-hover.gif)
+
+### Dropdowns
+
+The dropdown menu should feel like it is appearing from the triggering element. Combining a position shift `400ms cubic-bezier(0.23, 1, 0.32, 1)` with a opacity animation `200ms linear` on the second half of the motion achieves this affect.
+
+View the [interactive example](http://codepen.io/awhildy/full/jVLJpb/) here.
+
+![Dropdown animation](img/animation-dropdown.gif)
+
+### Quick update
+
+When information is updating in place, a quick, subtle animation is needed. The previous content should cut out, and the new content should have a quick, `200ms linear` fade in.
+
+![Quick update animation](img/animation-quickupdate.gif)
+
+> TODO: Add guidance for other kinds of animation \ No newline at end of file
diff --git a/doc/development/ux_guide/basics.md b/doc/development/ux_guide/basics.md
new file mode 100644
index 00000000000..e81729556d8
--- /dev/null
+++ b/doc/development/ux_guide/basics.md
@@ -0,0 +1,77 @@
+# Basics
+
+## Contents
+* [Responsive](#responsive)
+* [Typography](#typography)
+* [Icons](#icons)
+* [Color](#color)
+* [Cursors](#cursors)
+
+---
+
+## Responsive
+GitLab is a responsive experience that works well across all screen sizes, from mobile devices to large monitors. In order to provide a great user experience, the core functionality (browsing files, creating issues, writing comments, etc.) must be available at all resolutions. However, due to size limitations, some secondary functionality may be hidden on smaller screens. Please keep this functionality limited to rare actions that aren't expected to be needed on small devices.
+
+---
+
+## Typography
+### Primary typeface
+GitLab's main typeface used throughout the UI is **Source Sans Pro**. We support both the bold and regular weight.
+
+![Source Sans Pro sample](img/sourcesanspro-sample.png)
+
+
+### Monospace typeface
+This is the typeface used for code blocks. GitLab uses the OS default font.
+- **Menlo** (Mac)
+- **Consolas** (Windows)
+- **Liberation Mono** (Linux)
+
+![Monospace font sample](img/monospacefont-sample.png)
+
+---
+
+## Icons
+GitLab uses Font Awesome icons throughout our interface.
+
+| | |
+| :-----------: | :---- |
+| ![Trash icon](img/icon-trash.png) | The trash icon is used for destructive actions that deletes information. |
+| ![Edit icon](img/icon-edit.png) | The pencil icon is used for editing content such as comments.|
+| ![Notification icon](img/icon-notification.png) | The bell icon is for notifications, such as Todos. |
+| ![Subscribe icon](img/icon-subscribe.png) | The eye icon is for subscribing to updates. For example, you can subscribe to a label and get updated on issues with that label. |
+| ![RSS icon](img/icon-rss.png) | The standard RSS icon is used for linking to RSS/atom feeds. |
+| ![Close icon](img/icon-close.png) | An 'x' is used for closing UI elements such as dropdowns. |
+| ![Add icon](img/icon-add.png) | A plus is used when creating new objects, such as issues, projects, etc. |
+
+> TODO: update this section, add more general guidance to icon usage and personality, etc.
+
+---
+
+## Color
+
+| | |
+| :------: | :------- |
+| ![Blue](img/color-blue.png) | Blue is used to highlight primary active elements (such as the current tab), as well as other organizational and managing commands.|
+| ![Green](img/color-green.png) | Green is for actions that create new objects. |
+| ![Orange](img/color-orange.png) | Orange is used for warnings |
+| ![Red](img/color-red.png) | Red is reserved for delete and other destructive commands |
+| ![Grey](img/color-grey.png) | Grey is used for neutral secondary elements. Depending on context, white is sometimes used instead. |
+
+> TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage.
+
+---
+
+## Cursors
+The mouse cursor is key in helping users understand how to interact with elements on the screen.
+
+| | |
+| :------: | :------- |
+| ![Default cursor](img/cursors-default.png) | Default cursor |
+| ![Pointer cursor](img/cursors-pointer.png) | Pointer cursor: used to indicate that you can click on an element to invoke a command or navigate, such as links and buttons |
+| ![Move cursor](img/cursors-move.png) | Move cursor: used to indicate that you can move an element around on the screen |
+| ![Pan opened cursor](img/cursors-panopened.png) | Pan cursor (opened): indicates that you can grab and move the entire canvas, affecting what is seen in the view port. |
+| ![Pan closed cursor](img/cursors-panclosed.png) | Pan cursor (closed): indicates that you are actively panning the canvas. |
+| ![I-beam cursor](img/cursors-ibeam.png) | I-beam cursor: indicates that this is either text that you can select and copy, or a text field that you can click into to enter text. |
+
+
diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md
new file mode 100644
index 00000000000..28383ad7dfc
--- /dev/null
+++ b/doc/development/ux_guide/components.md
@@ -0,0 +1,267 @@
+# Components
+
+## Contents
+* [Tooltips](#tooltips)
+* [Anchor links](#anchor-links)
+* [Buttons](#buttons)
+* [Dropdowns](#dropdowns)
+* [Counts](#counts)
+* [Lists](#lists)
+* [Tables](#tables)
+* [Blocks](#blocks)
+* [Panels](#panels)
+* [Alerts](#alerts)
+* [Forms](#forms)
+* [File holders](#file-holders)
+* [Data formats](#data-formats)
+
+---
+
+## Tooltips
+
+### Usage
+A tooltip should only be added if additional information is required.
+
+![Tooltip usage](img/tooltip-usage.png)
+
+### Placement
+By default, tooltips should be placed below the element that they refer to. However, if there is not enough space in the viewpoint, the tooltip should be moved to the side as needed.
+
+![Tooltip placement location](img/tooltip-placement.png)
+
+---
+
+## Anchor links
+
+Anchor links are used for navigational actions and lone, secondary commands (such as 'Reset filters' on the Issues List) when deemed appropriate by the UX team.
+
+### States
+
+#### Rest
+
+Primary links are blue in their rest state. Secondary links (such as the time stamp on comments) are a neutral gray color in rest. Details on the main GitLab navigation links can be found on the [features](features.md#navigation) page.
+
+#### Hover
+
+On hover, an underline should be added and the color should change. Both the primary and secondary link should become the darker blue color on hover.
+
+#### Focus
+
+The focus state should match the hover state.
+
+![Anchor link states ](img/components-anchorlinks.png)
+
+---
+
+## Buttons
+
+Buttons communicate the command that will occur when the user clicks on them.
+
+### Types
+
+#### Primary
+Primary buttons communicate the main call to action. There should only be one call to action in any given experience. Visually, primary buttons are conveyed with a full background fill
+
+![Primary button example](img/button-primary.png)
+
+#### Secondary
+Secondary buttons are for alternative commands. They should be conveyed by a button with an stroke, and no background fill.
+
+![Secondary button example](img/button-secondary.png)
+
+### Icon and text treatment
+Text should be in sentence case, where only the first word is capitalized. "Create issue" is correct, not "Create Issue". Buttons should only contain an icon or a text, not both.
+
+> TODO: Rationalize this. Ensure that we still believe this.
+
+### Colors
+The default color treatment is the white/grey button. Follow the guidance on the [basics](basics.md#color) page to add meaningful color to a button.
+
+### Secondary states
+
+Primary buttons darken the color of their background and border for hover, focus and active states. An inner shadow is added to the active state to denote the button being pressed.
+
+| Values | Info | Success | Warning | Danger |
+| :------ | :------: | :------: | :------: | :------: |
+| Background: `$color-light` <br> Border: `$border-color-light` | ![](img/button-info--resting.png) | ![](img/button-success--resting.png) | ![](img/button-warning--resting.png) | ![](img/button-danger--resting.png) |
+| Background: `$color-normal` <br> Border: `$border-color-normal` | ![](img/button-info--hover.png) | ![](img/button-success--hover.png) | ![](img/button-warning--hover.png) | ![](img/button-danger--hover.png) |
+| Background: `$color-dark` <br> Border: `$border-color-dark` | ![](img/button-info--active.png) | ![](img/button-success--active.png) | ![](img/button-warning--active.png) | ![](img/button-danger--active.png) |
+
+Since secondary buttons only have a border on their resting state, their hover and focus states add a background color, which gets darkened on active.
+
+| Values | Success Secondary | Close | Spam |
+| :------ | :------: | :------: | :------: |
+| Font: `$border-color-light` <br> Border: `$border-color-light` | ![](img/button-success-secondary--resting.png) | ![](img/button-close--resting.png) | ![](img/button-spam--resting.png) |
+| 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) |
+
+---
+
+
+## Dropdowns
+
+Dropdowns are used to allow users to choose one (or many) options from a list of options. If this list of options is more 20, there should generally be a way to search through and filter the options (see the complex filter dropdowns below.)
+
+> TODO: Will update this section when the new filters UI is implemented.
+
+![Dropdown states](img/components-dropdown.png)
+
+### Max size
+
+The max height for dropdowns should target **10-15 items**. If the height of the dropdown is too large, the list becomes very hard to parse and it is easy to visually lose track of the item you are looking for. Usability also suffers as more mouse movement is required, and you have a larger area in which you hijack the scroll away from the page level. While it may initially seem counterintuitive to not show as many items as you can, it is actually quicker and easier to process the information when it is cropped at a reasonable height.
+
+---
+
+## Counts
+
+A count element is used in navigation contexts where it is helpful to indicate the count, or number of items, in a list. Always use the [`number_with_delimiter`][number_with_delimiter] helper to display counts in the UI.
+
+![Counts example](img/components-counts.png)
+
+[number_with_delimiter]: http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_with_delimiter
+
+---
+
+## Lists
+
+Lists are used where ever there is a single column of information to display. Ths [issues list](https://gitlab.com/gitlab-org/gitlab-ce/issues) is an example of a important list in the GitLab UI.
+
+### Types
+
+Simple list using .content-list
+
+![Simple list](img/components-simplelist.png)
+
+List with avatar, title and description using .content-list
+
+![List with avatar](img/components-listwithavatar.png)
+
+List with hover effect .well-list
+
+![List with hover effect](img/components-listwithhover.png)
+
+List inside panel
+
+![List inside panel](img/components-listinsidepanel.png)
+
+---
+
+## Tables
+
+When the information is too complex for a list, with multiple columns of information, a table can be used. For example, the [pipelines page](https://gitlab.com/gitlab-org/gitlab-ce/pipelines) uses a table.
+
+![Table](img/components-table.png)
+
+---
+
+## Blocks
+
+Blocks are a way to group related information.
+
+### Types
+
+#### Content blocks
+
+Content blocks (`.content-block`) are the basic grouping of content. They are commonly used in [lists](#lists), and are separated by a botton border.
+
+![Content block](img/components-contentblock.png)
+
+#### Row content blocks
+
+A background color can be added to this blocks. For example, items in the [issue list](https://gitlab.com/gitlab-org/gitlab-ce/issues) have a green background if they were created recently. Below is an example of a gray content block with side padding using `.row-content-block`.
+
+![Row content block](img/components-rowcontentblock.png)
+
+#### Cover blocks
+Cover blocks are generally used to create a heading element for a page, such as a new project, or a user profile page. Below is a cover block (`.cover-block`) for the profile page with an avatar, name and description.
+
+![Cover block](img/components-coverblock.png)
+
+---
+
+## Panels
+
+> TODO: Catalog how we are currently using panels and rationalize how they relate to alerts
+
+![Panels](img/components-panels.png)
+
+---
+
+## Alerts
+
+> TODO: Catalog how we are currently using alerts
+
+![Alerts](img/components-alerts.png)
+
+---
+
+## Forms
+
+There are two options shown below regarding the positioning of labels in forms. Both are options to consider based on context and available size. However, it is important to have a consistent treatment of labels in the same form.
+
+### Types
+
+#### Labels stack vertically
+
+Form (`form`) with label rendered above input.
+
+![Vertical form](img/components-verticalform.png)
+
+#### Labels side-by-side
+
+Horizontal form (`form.horizontal-form`) with label rendered inline with input.
+
+![Horizontal form](img/components-horizontalform.png)
+
+---
+
+## File holders
+A file holder (`.file-holder`) is used to show the contents of a file inline on a page of GitLab.
+
+![File Holder component](img/components-fileholder.png)
+
+---
+
+## Data formats
+
+### Dates
+
+#### Exact
+
+Format for exacts dates should be ‘Mon DD, YYYY’, such as the examples below.
+
+![Exact date](img/components-dateexact.png)
+
+#### Relative
+
+This format relates how long since an action has occurred. The exact date can be shown as a tooltip on hover.
+
+![Relative date](img/components-daterelative.png)
+
+### References
+
+Referencing GitLab items depends on a symbol for each type of item. Typing that symbol will invoke a dropdown that allows you to search for and autocomplete the item you were looking for. References are shown as [links](#links) in context, and hovering on them shows the full title or name of the item.
+
+![Hovering on a reference](img/components-referencehover.png)
+
+#### `%` Milestones
+
+![Milestone reference](img/components-referencemilestone.png)
+
+#### `#` Issues
+
+![Issue reference](img/components-referenceissues.png)
+
+#### `!` Merge Requests
+
+![Merge request reference](img/components-referencemrs.png)
+
+#### `~` Labels
+
+![Labels reference](img/components-referencelabels.png)
+
+#### `@` People
+
+![People reference](img/components-referencepeople.png)
+
+> TODO: Open issue: Some commit references use monospace fonts, but others don't. Need to standardize this.
diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md
new file mode 100644
index 00000000000..8896d450f14
--- /dev/null
+++ b/doc/development/ux_guide/copy.md
@@ -0,0 +1,103 @@
+# Copy
+
+The copy for GitLab is clear and direct. We strike a clear balance between professional and friendly. We can empathesize with users (such as celebrating completing all Todos), and remain respectful of the importance of the work. We are that trusted, friendly coworker that is helpful and understanding.
+
+The copy and messaging is a core part of the experience of GitLab and the conversation with our users. Follow the below conventions throughout GitLab.
+
+>**Note:**
+We are currently inconsistent with this guidance. Images below are created to illustrate the point. As this guidance is refined, we will ensure that our experiences align.
+
+## Contents
+* [Brevity](#brevity)
+* [Sentence case](#sentence-case)
+* [Terminology](#terminology)
+
+---
+
+## Brevity
+Users will skim content, rather than read text carefully.
+When familiar with a web app, users rely on muscle memory, and may read even less when moving quickly.
+A good experience should quickly orient a user, regardless of their experience, to the purpose of the current screen. This should happen without the user having to consciously read long strings of text.
+In general, text is burdensome and adds cognitive load. This is especially pronounced in a powerful productivity tool such as GitLab.
+We should _not_ rely on words as a crutch to explain the purpose of a screen.
+The current navigation and composition of the elements on the screen should get the user 95% there, with the remaining 5% being specific elements such as text.
+This means that, as a rule, copy should be very short. A long message or label is a red flag hinting at design that needs improvement.
+
+>**Example:**
+Use `Add` instead of `Add issue` as a button label.
+Preferrably use context and placement of controls to make it obvious what clicking on them will do.
+
+---
+
+## Sentence case
+Use sentence case for all titles, headings, labels, menu items, and buttons.
+
+---
+
+## Terminology
+Only use the terms in the tables below.
+
+### Issues
+
+#### Adjectives (states)
+
+| Term |
+| ---- |
+| Open |
+| Closed |
+| Deleted |
+
+>**Example:**
+Use `5 open issues` and don't use `5 pending issues`.
+
+#### Verbs (actions)
+
+| Term | Use | Don't |
+| ---- | --- | --- |
+| Add | Add an issue | Don't use `create` or `new` |
+| View | View an open or closed issue ||
+| Edit | Edit an open or closed issue | Don't use `update` |
+| Close | Close an open issue ||
+| Re-open | Re-open a closed issue | There should never be a need to use `open` as a verb |
+| Delete | Delete an open or closed issue ||
+
+#### Add issue
+
+When viewing a list of issues, there is a button that is labeled `Add`. Given the context in the example, it is clearly referring to issues. If the context were not clear enough, the label could be `Add issue`. Clicking the button will bring you to the `Add issue` form. Other add flows should be similar.
+
+![Add issue button](img/copy-form-addissuebutton.png)
+
+The form should be titled `Add issue`. The submit button should be labeled `Submit`. Don't use `Add`, `Create`, `New`, or `Save changes`. The cancel button should be labeled `Cancel`. Don't use `Back`.
+
+![Add issue form](img/copy-form-addissueform.png)
+
+#### Edit issue
+
+When in context of an issue, the affordance to edit it is labeled `Edit`. If the context is not clear enough, `Edit issue` could be considered. Other edit flows should be similar.
+
+![Edit issue button](img/copy-form-editissuebutton.png)
+
+The form should be titled `Edit issue`. The submit button should be labeled `Save`. Don't use `Edit`, `Update`, `Submit`, or `Save changes`. The cancel button should be labeled `Cancel`. Don't use `Back`.
+
+![Edit issue form](img/copy-form-editissueform.png)
+
+
+### Merge requests
+
+#### Adjectives (states)
+
+| Term |
+| ---- |
+| Open |
+| Merged |
+
+#### Verbs (actions)
+
+| Term | Use | Don't |
+| ---- | --- | --- |
+| Add | Add a merge request | Do not use `create` or `new` |
+| View | View an open or merged merge request ||
+| Edit | Edit an open or merged merge request| Do not use `update` |
+| Approve | Approve an open merge request ||
+| Remove approval, unapproved | Remove approval of an open merge request | Do not use `unapprove` as that is not an English word|
+| Merge | Merge an open merge request ||
diff --git a/doc/development/ux_guide/features.md b/doc/development/ux_guide/features.md
new file mode 100644
index 00000000000..9472995c68c
--- /dev/null
+++ b/doc/development/ux_guide/features.md
@@ -0,0 +1,57 @@
+# Features
+
+## Contents
+* [Navigation](#navigation)
+* [Filtering](#filtering)
+* [Search results](#search-results)
+* [Conversations](#conversations)
+* [Empty states](#empty-states)
+
+---
+
+## Navigation
+
+### Global navigation
+
+The global navigation is accessible via the menu button on the top left of the screen, and can be pinned to keep it open. It contains a consistent list of pages that allow you to view content that is across GitLab. For example, you can view your todos, issues and merge requests across projects and groups.
+
+![Global nav](img/features-globalnav.png)
+
+
+### Contextual navigation
+
+The navigation in the header is contextual to each page. These options change depending on if you are looking at a project, group, or settings page. There should be no more than 10 items on a level in the contextual navigation, allowing it to comfortably fit on a typical laptop screen. There can be up to too levels of navigation. Each sub nav group should be a self-contained group of functionality. For example, everything related to the issue tracker should be under the 'Issue' tab, while everything relating to the wiki will be grouped under the 'Wiki' tab. The names used for each section should be short and easy to remember, ideally 1-2 words in length.
+
+![Contextual nav](img/features-contextualnav.png)
+
+### Information architecture
+
+The [GitLab Product Map](https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png) shows a visual representation of the information architecture for GitLab.
+
+---
+
+## Filtering
+
+Today, lists are filtered by a series of dropdowns. Some of these dropdowns allow multiselect (labels), while others allow you to filter to one option (milestones). However, we are currently implementing a [new model](https://gitlab.com/gitlab-org/gitlab-ce/issues/21747) for this, and will update the guide when it is ready.
+
+![Filters](img/features-filters.png)
+
+---
+
+## Search results
+
+### Global search
+
+[Global search](https://gitlab.com/search?group_id=&project_id=13083&repository_ref=&scope=issues&search=mobile) allows you to search across items in a project, or even across multiple projects. You can switch tabs to filter on type of object, or filter by group.
+
+### List search
+
+There are several core lists in the GitLab experience, such as the Issue list and the Merge Request list. You are also able to [filter and search these lists](https://gitlab.com/gitlab-org/gitlab-ce/issues?utf8=%E2%9C%93&search=mobile). This UI will be updated with the [new filtering model](https://gitlab.com/gitlab-org/gitlab-ce/issues/21747).
+
+---
+
+## Empty states
+
+Empty states need to be considered in the design of features. They are vital to helping onboard new users, making the experience feel more approachable and understandable. Empty states should feel inviting and provide just enough information to get people started. There should be a single call to action and a clear explanation of what to use the feature for.
+
+![Empty states](img/features-emptystates.png)
diff --git a/doc/development/ux_guide/img/animation-dropdown.gif b/doc/development/ux_guide/img/animation-dropdown.gif
new file mode 100644
index 00000000000..c9b31d26165
--- /dev/null
+++ b/doc/development/ux_guide/img/animation-dropdown.gif
Binary files differ
diff --git a/doc/development/ux_guide/img/animation-hover.gif b/doc/development/ux_guide/img/animation-hover.gif
new file mode 100644
index 00000000000..37ad9c76828
--- /dev/null
+++ b/doc/development/ux_guide/img/animation-hover.gif
Binary files differ
diff --git a/doc/development/ux_guide/img/animation-quickupdate.gif b/doc/development/ux_guide/img/animation-quickupdate.gif
new file mode 100644
index 00000000000..8db70bc3d24
--- /dev/null
+++ b/doc/development/ux_guide/img/animation-quickupdate.gif
Binary files differ
diff --git a/doc/development/ux_guide/img/button-close--active.png b/doc/development/ux_guide/img/button-close--active.png
new file mode 100644
index 00000000000..824bfc8f31b
--- /dev/null
+++ b/doc/development/ux_guide/img/button-close--active.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-close--hover.png b/doc/development/ux_guide/img/button-close--hover.png
new file mode 100644
index 00000000000..0291e121894
--- /dev/null
+++ b/doc/development/ux_guide/img/button-close--hover.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-close--resting.png b/doc/development/ux_guide/img/button-close--resting.png
new file mode 100644
index 00000000000..986d7174ce7
--- /dev/null
+++ b/doc/development/ux_guide/img/button-close--resting.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-danger--active.png b/doc/development/ux_guide/img/button-danger--active.png
new file mode 100644
index 00000000000..d3c64424b26
--- /dev/null
+++ b/doc/development/ux_guide/img/button-danger--active.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-danger--hover.png b/doc/development/ux_guide/img/button-danger--hover.png
new file mode 100644
index 00000000000..8506e093306
--- /dev/null
+++ b/doc/development/ux_guide/img/button-danger--hover.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-danger--resting.png b/doc/development/ux_guide/img/button-danger--resting.png
new file mode 100644
index 00000000000..69ad6bb796b
--- /dev/null
+++ b/doc/development/ux_guide/img/button-danger--resting.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-info--active.png b/doc/development/ux_guide/img/button-info--active.png
new file mode 100644
index 00000000000..23be20b225c
--- /dev/null
+++ b/doc/development/ux_guide/img/button-info--active.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-info--hover.png b/doc/development/ux_guide/img/button-info--hover.png
new file mode 100644
index 00000000000..4cb4e38558c
--- /dev/null
+++ b/doc/development/ux_guide/img/button-info--hover.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-info--resting.png b/doc/development/ux_guide/img/button-info--resting.png
new file mode 100644
index 00000000000..5883340aa83
--- /dev/null
+++ b/doc/development/ux_guide/img/button-info--resting.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-primary.png b/doc/development/ux_guide/img/button-primary.png
new file mode 100644
index 00000000000..eda5ed84aec
--- /dev/null
+++ b/doc/development/ux_guide/img/button-primary.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-secondary.png b/doc/development/ux_guide/img/button-secondary.png
new file mode 100644
index 00000000000..26d4e8cf43d
--- /dev/null
+++ b/doc/development/ux_guide/img/button-secondary.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-spam--active.png b/doc/development/ux_guide/img/button-spam--active.png
new file mode 100644
index 00000000000..55b44898684
--- /dev/null
+++ b/doc/development/ux_guide/img/button-spam--active.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-spam--hover.png b/doc/development/ux_guide/img/button-spam--hover.png
new file mode 100644
index 00000000000..3dc8ed34c54
--- /dev/null
+++ b/doc/development/ux_guide/img/button-spam--hover.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-spam--resting.png b/doc/development/ux_guide/img/button-spam--resting.png
new file mode 100644
index 00000000000..b6bf10a5b64
--- /dev/null
+++ b/doc/development/ux_guide/img/button-spam--resting.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-success--active.png b/doc/development/ux_guide/img/button-success--active.png
new file mode 100644
index 00000000000..895a52831cb
--- /dev/null
+++ b/doc/development/ux_guide/img/button-success--active.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-success--hover.png b/doc/development/ux_guide/img/button-success--hover.png
new file mode 100644
index 00000000000..e4c74bd9778
--- /dev/null
+++ b/doc/development/ux_guide/img/button-success--hover.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-success--resting.png b/doc/development/ux_guide/img/button-success--resting.png
new file mode 100644
index 00000000000..2fa971b5347
--- /dev/null
+++ b/doc/development/ux_guide/img/button-success--resting.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-success-secondary--active.png b/doc/development/ux_guide/img/button-success-secondary--active.png
new file mode 100644
index 00000000000..e7383b36946
--- /dev/null
+++ b/doc/development/ux_guide/img/button-success-secondary--active.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-success-secondary--hover.png b/doc/development/ux_guide/img/button-success-secondary--hover.png
new file mode 100644
index 00000000000..4af2a68cf1b
--- /dev/null
+++ b/doc/development/ux_guide/img/button-success-secondary--hover.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-success-secondary--resting.png b/doc/development/ux_guide/img/button-success-secondary--resting.png
new file mode 100644
index 00000000000..a5a4ec512c8
--- /dev/null
+++ b/doc/development/ux_guide/img/button-success-secondary--resting.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-warning--active.png b/doc/development/ux_guide/img/button-warning--active.png
new file mode 100644
index 00000000000..5877d46c94d
--- /dev/null
+++ b/doc/development/ux_guide/img/button-warning--active.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-warning--hover.png b/doc/development/ux_guide/img/button-warning--hover.png
new file mode 100644
index 00000000000..308e1adc8a3
--- /dev/null
+++ b/doc/development/ux_guide/img/button-warning--hover.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-warning--resting.png b/doc/development/ux_guide/img/button-warning--resting.png
new file mode 100644
index 00000000000..28e5e601520
--- /dev/null
+++ b/doc/development/ux_guide/img/button-warning--resting.png
Binary files differ
diff --git a/doc/development/ux_guide/img/color-blue.png b/doc/development/ux_guide/img/color-blue.png
new file mode 100644
index 00000000000..844e926f1f5
--- /dev/null
+++ b/doc/development/ux_guide/img/color-blue.png
Binary files differ
diff --git a/doc/development/ux_guide/img/color-green.png b/doc/development/ux_guide/img/color-green.png
new file mode 100644
index 00000000000..5c4c23c7067
--- /dev/null
+++ b/doc/development/ux_guide/img/color-green.png
Binary files differ
diff --git a/doc/development/ux_guide/img/color-grey.png b/doc/development/ux_guide/img/color-grey.png
new file mode 100644
index 00000000000..5247649a0ce
--- /dev/null
+++ b/doc/development/ux_guide/img/color-grey.png
Binary files differ
diff --git a/doc/development/ux_guide/img/color-orange.png b/doc/development/ux_guide/img/color-orange.png
new file mode 100644
index 00000000000..1103c715225
--- /dev/null
+++ b/doc/development/ux_guide/img/color-orange.png
Binary files differ
diff --git a/doc/development/ux_guide/img/color-red.png b/doc/development/ux_guide/img/color-red.png
new file mode 100644
index 00000000000..77ecbbc0a20
--- /dev/null
+++ b/doc/development/ux_guide/img/color-red.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-alerts.png b/doc/development/ux_guide/img/components-alerts.png
new file mode 100644
index 00000000000..66a43ac69e1
--- /dev/null
+++ b/doc/development/ux_guide/img/components-alerts.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-anchorlinks.png b/doc/development/ux_guide/img/components-anchorlinks.png
new file mode 100644
index 00000000000..4a9c730566c
--- /dev/null
+++ b/doc/development/ux_guide/img/components-anchorlinks.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-contentblock.png b/doc/development/ux_guide/img/components-contentblock.png
new file mode 100644
index 00000000000..58d87729701
--- /dev/null
+++ b/doc/development/ux_guide/img/components-contentblock.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-counts.png b/doc/development/ux_guide/img/components-counts.png
new file mode 100644
index 00000000000..19280e988a0
--- /dev/null
+++ b/doc/development/ux_guide/img/components-counts.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-coverblock.png b/doc/development/ux_guide/img/components-coverblock.png
new file mode 100644
index 00000000000..fb135f9648a
--- /dev/null
+++ b/doc/development/ux_guide/img/components-coverblock.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-dateexact.png b/doc/development/ux_guide/img/components-dateexact.png
new file mode 100644
index 00000000000..686ca727293
--- /dev/null
+++ b/doc/development/ux_guide/img/components-dateexact.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-daterelative.png b/doc/development/ux_guide/img/components-daterelative.png
new file mode 100644
index 00000000000..4954dfb51b3
--- /dev/null
+++ b/doc/development/ux_guide/img/components-daterelative.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-dropdown.png b/doc/development/ux_guide/img/components-dropdown.png
new file mode 100644
index 00000000000..7f9a701c089
--- /dev/null
+++ b/doc/development/ux_guide/img/components-dropdown.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-fileholder.png b/doc/development/ux_guide/img/components-fileholder.png
new file mode 100644
index 00000000000..ec2911a1232
--- /dev/null
+++ b/doc/development/ux_guide/img/components-fileholder.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-horizontalform.png b/doc/development/ux_guide/img/components-horizontalform.png
new file mode 100644
index 00000000000..c57dceda43a
--- /dev/null
+++ b/doc/development/ux_guide/img/components-horizontalform.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-listinsidepanel.png b/doc/development/ux_guide/img/components-listinsidepanel.png
new file mode 100644
index 00000000000..3a72d39bb5d
--- /dev/null
+++ b/doc/development/ux_guide/img/components-listinsidepanel.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-listwithavatar.png b/doc/development/ux_guide/img/components-listwithavatar.png
new file mode 100644
index 00000000000..f6db575433c
--- /dev/null
+++ b/doc/development/ux_guide/img/components-listwithavatar.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-listwithhover.png b/doc/development/ux_guide/img/components-listwithhover.png
new file mode 100644
index 00000000000..8521a8ad53e
--- /dev/null
+++ b/doc/development/ux_guide/img/components-listwithhover.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-panels.png b/doc/development/ux_guide/img/components-panels.png
new file mode 100644
index 00000000000..c1391ca07e5
--- /dev/null
+++ b/doc/development/ux_guide/img/components-panels.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-referencehover.png b/doc/development/ux_guide/img/components-referencehover.png
new file mode 100644
index 00000000000..f80564dbb16
--- /dev/null
+++ b/doc/development/ux_guide/img/components-referencehover.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-referenceissues.png b/doc/development/ux_guide/img/components-referenceissues.png
new file mode 100644
index 00000000000..51fb2cf3e43
--- /dev/null
+++ b/doc/development/ux_guide/img/components-referenceissues.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-referencelabels.png b/doc/development/ux_guide/img/components-referencelabels.png
new file mode 100644
index 00000000000..aba450cc3ba
--- /dev/null
+++ b/doc/development/ux_guide/img/components-referencelabels.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-referencemilestone.png b/doc/development/ux_guide/img/components-referencemilestone.png
new file mode 100644
index 00000000000..adf2555ccf8
--- /dev/null
+++ b/doc/development/ux_guide/img/components-referencemilestone.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-referencemrs.png b/doc/development/ux_guide/img/components-referencemrs.png
new file mode 100644
index 00000000000..6c3375f1ea1
--- /dev/null
+++ b/doc/development/ux_guide/img/components-referencemrs.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-referencepeople.png b/doc/development/ux_guide/img/components-referencepeople.png
new file mode 100644
index 00000000000..b8dd431e2e6
--- /dev/null
+++ b/doc/development/ux_guide/img/components-referencepeople.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-rowcontentblock.png b/doc/development/ux_guide/img/components-rowcontentblock.png
new file mode 100644
index 00000000000..c66a50f9564
--- /dev/null
+++ b/doc/development/ux_guide/img/components-rowcontentblock.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-simplelist.png b/doc/development/ux_guide/img/components-simplelist.png
new file mode 100644
index 00000000000..858e5064c25
--- /dev/null
+++ b/doc/development/ux_guide/img/components-simplelist.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-table.png b/doc/development/ux_guide/img/components-table.png
new file mode 100644
index 00000000000..cedc55758a9
--- /dev/null
+++ b/doc/development/ux_guide/img/components-table.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-verticalform.png b/doc/development/ux_guide/img/components-verticalform.png
new file mode 100644
index 00000000000..489ae6f862f
--- /dev/null
+++ b/doc/development/ux_guide/img/components-verticalform.png
Binary files differ
diff --git a/doc/development/ux_guide/img/copy-form-addissuebutton.png b/doc/development/ux_guide/img/copy-form-addissuebutton.png
new file mode 100644
index 00000000000..8457f0ab2ab
--- /dev/null
+++ b/doc/development/ux_guide/img/copy-form-addissuebutton.png
Binary files differ
diff --git a/doc/development/ux_guide/img/copy-form-addissueform.png b/doc/development/ux_guide/img/copy-form-addissueform.png
new file mode 100644
index 00000000000..89c6b4acdfb
--- /dev/null
+++ b/doc/development/ux_guide/img/copy-form-addissueform.png
Binary files differ
diff --git a/doc/development/ux_guide/img/copy-form-editissuebutton.png b/doc/development/ux_guide/img/copy-form-editissuebutton.png
new file mode 100644
index 00000000000..04bcc2bf831
--- /dev/null
+++ b/doc/development/ux_guide/img/copy-form-editissuebutton.png
Binary files differ
diff --git a/doc/development/ux_guide/img/copy-form-editissueform.png b/doc/development/ux_guide/img/copy-form-editissueform.png
new file mode 100644
index 00000000000..126ef34ea7e
--- /dev/null
+++ b/doc/development/ux_guide/img/copy-form-editissueform.png
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-default.png b/doc/development/ux_guide/img/cursors-default.png
new file mode 100644
index 00000000000..c188ec4e351
--- /dev/null
+++ b/doc/development/ux_guide/img/cursors-default.png
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-ibeam.png b/doc/development/ux_guide/img/cursors-ibeam.png
new file mode 100644
index 00000000000..86f28639982
--- /dev/null
+++ b/doc/development/ux_guide/img/cursors-ibeam.png
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-move.png b/doc/development/ux_guide/img/cursors-move.png
new file mode 100644
index 00000000000..a9c610eaa88
--- /dev/null
+++ b/doc/development/ux_guide/img/cursors-move.png
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-panclosed.png b/doc/development/ux_guide/img/cursors-panclosed.png
new file mode 100644
index 00000000000..6d247a765ac
--- /dev/null
+++ b/doc/development/ux_guide/img/cursors-panclosed.png
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-panopened.png b/doc/development/ux_guide/img/cursors-panopened.png
new file mode 100644
index 00000000000..76f2eeda831
--- /dev/null
+++ b/doc/development/ux_guide/img/cursors-panopened.png
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-pointer.png b/doc/development/ux_guide/img/cursors-pointer.png
new file mode 100644
index 00000000000..d86dd955fa7
--- /dev/null
+++ b/doc/development/ux_guide/img/cursors-pointer.png
Binary files differ
diff --git a/doc/development/ux_guide/img/features-contextualnav.png b/doc/development/ux_guide/img/features-contextualnav.png
new file mode 100644
index 00000000000..f8466f28627
--- /dev/null
+++ b/doc/development/ux_guide/img/features-contextualnav.png
Binary files differ
diff --git a/doc/development/ux_guide/img/features-emptystates.png b/doc/development/ux_guide/img/features-emptystates.png
new file mode 100644
index 00000000000..51835a7080b
--- /dev/null
+++ b/doc/development/ux_guide/img/features-emptystates.png
Binary files differ
diff --git a/doc/development/ux_guide/img/features-filters.png b/doc/development/ux_guide/img/features-filters.png
new file mode 100644
index 00000000000..41db76db938
--- /dev/null
+++ b/doc/development/ux_guide/img/features-filters.png
Binary files differ
diff --git a/doc/development/ux_guide/img/features-globalnav.png b/doc/development/ux_guide/img/features-globalnav.png
new file mode 100644
index 00000000000..73294d1b524
--- /dev/null
+++ b/doc/development/ux_guide/img/features-globalnav.png
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-add.png b/doc/development/ux_guide/img/icon-add.png
new file mode 100644
index 00000000000..bcad5e84591
--- /dev/null
+++ b/doc/development/ux_guide/img/icon-add.png
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-close.png b/doc/development/ux_guide/img/icon-close.png
new file mode 100644
index 00000000000..dfe1495f5fa
--- /dev/null
+++ b/doc/development/ux_guide/img/icon-close.png
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-edit.png b/doc/development/ux_guide/img/icon-edit.png
new file mode 100644
index 00000000000..50f6f841868
--- /dev/null
+++ b/doc/development/ux_guide/img/icon-edit.png
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-notification.png b/doc/development/ux_guide/img/icon-notification.png
new file mode 100644
index 00000000000..6ddfaa44f66
--- /dev/null
+++ b/doc/development/ux_guide/img/icon-notification.png
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-rss.png b/doc/development/ux_guide/img/icon-rss.png
new file mode 100644
index 00000000000..b766488b32d
--- /dev/null
+++ b/doc/development/ux_guide/img/icon-rss.png
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-subscribe.png b/doc/development/ux_guide/img/icon-subscribe.png
new file mode 100644
index 00000000000..650168296c6
--- /dev/null
+++ b/doc/development/ux_guide/img/icon-subscribe.png
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-trash.png b/doc/development/ux_guide/img/icon-trash.png
new file mode 100644
index 00000000000..b02178ca992
--- /dev/null
+++ b/doc/development/ux_guide/img/icon-trash.png
Binary files differ
diff --git a/doc/development/ux_guide/img/monospacefont-sample.png b/doc/development/ux_guide/img/monospacefont-sample.png
new file mode 100644
index 00000000000..1cd290b713c
--- /dev/null
+++ b/doc/development/ux_guide/img/monospacefont-sample.png
Binary files differ
diff --git a/doc/development/ux_guide/img/sourcesanspro-sample.png b/doc/development/ux_guide/img/sourcesanspro-sample.png
new file mode 100644
index 00000000000..f7ecf0c7c66
--- /dev/null
+++ b/doc/development/ux_guide/img/sourcesanspro-sample.png
Binary files differ
diff --git a/doc/development/ux_guide/img/surfaces-contentitemtitle.png b/doc/development/ux_guide/img/surfaces-contentitemtitle.png
new file mode 100644
index 00000000000..3af0b56c8fb
--- /dev/null
+++ b/doc/development/ux_guide/img/surfaces-contentitemtitle.png
Binary files differ
diff --git a/doc/development/ux_guide/img/surfaces-header.png b/doc/development/ux_guide/img/surfaces-header.png
new file mode 100644
index 00000000000..ba616388003
--- /dev/null
+++ b/doc/development/ux_guide/img/surfaces-header.png
Binary files differ
diff --git a/doc/development/ux_guide/img/surfaces-systeminformationblock.png b/doc/development/ux_guide/img/surfaces-systeminformationblock.png
new file mode 100644
index 00000000000..9f42f1d4dd0
--- /dev/null
+++ b/doc/development/ux_guide/img/surfaces-systeminformationblock.png
Binary files differ
diff --git a/doc/development/ux_guide/img/surfaces-ux.png b/doc/development/ux_guide/img/surfaces-ux.png
new file mode 100644
index 00000000000..53208727c64
--- /dev/null
+++ b/doc/development/ux_guide/img/surfaces-ux.png
Binary files differ
diff --git a/doc/development/ux_guide/img/tooltip-placement.png b/doc/development/ux_guide/img/tooltip-placement.png
new file mode 100644
index 00000000000..061f82e4df0
--- /dev/null
+++ b/doc/development/ux_guide/img/tooltip-placement.png
Binary files differ
diff --git a/doc/development/ux_guide/img/tooltip-usage.png b/doc/development/ux_guide/img/tooltip-usage.png
new file mode 100644
index 00000000000..40c4f051cd0
--- /dev/null
+++ b/doc/development/ux_guide/img/tooltip-usage.png
Binary files differ
diff --git a/doc/development/ux_guide/index.md b/doc/development/ux_guide/index.md
new file mode 100644
index 00000000000..8a849f239dc
--- /dev/null
+++ b/doc/development/ux_guide/index.md
@@ -0,0 +1,63 @@
+# GitLab UX Guide
+
+The goal of this guide is to provide standards, principles and in-depth information to design beautiful and effective GitLab features. This will be a living document, and we welcome contributions, feedback and suggestions.
+
+## Design
+
+---
+
+### [Principles](principles.md)
+These guiding principles set a solid foundation for our design system, and should remain relatively stable over multiple releases. They should be referenced as new design patterns are created.
+
+---
+
+### [Basics](basics.md)
+The basic ingredients of our experience establish our personality and feel. This section includes details about typography, iconography, and color.
+
+---
+
+### [Animation](animation.md)
+Guidance on the timing, curving and motion for GitLab.
+
+---
+
+### [Copy](copy.md)
+Conventions on text and messaging within labels, buttons, and other components.
+
+---
+
+### [Components](components.md)
+Components are the controls that make up the GitLab experience, including guidance around buttons, links, dropdowns, etc.
+
+---
+
+### [Surfaces](surfaces.md)
+The GitLab experience is broken apart into several surfaces. Each of these surfaces is designated for a specific scope or type of content. Examples include the header, global menu, side pane, etc.
+
+---
+
+### [Features](features.md)
+The previous building blocks are combined into complete features in the GitLab UX. Examples include our navigation, filters, search results, and empty states.
+
+---
+
+## Research
+
+---
+
+### [Users](users.md)
+How we think about the variety of users of GitLab, from small to large teams, comparing opensource usage to enterprise, etc.
+
+---
+
+## Other
+
+---
+
+### [Tips for designers](tips.md)
+Tips for exporting assets, and other guidance.
+
+---
+
+### [Resources](resources.md)
+Resources for GitLab UX
diff --git a/doc/development/ux_guide/principles.md b/doc/development/ux_guide/principles.md
new file mode 100644
index 00000000000..1a297cba2cc
--- /dev/null
+++ b/doc/development/ux_guide/principles.md
@@ -0,0 +1,17 @@
+# Principles
+
+These are the guiding principles that we should strive for to establish a solid foundation for the GitLab experience.
+
+## Professional and productive
+GitLab is a tool to support what people do, day in, day out. We need to respect the importance of their work, and avoid gimicky details.
+
+## Minimal and efficient
+While work can get complicated, GitLab is about bringing a sharp focus, helping our customers know what matters now.
+
+## Immediately recognizable
+When you look at any screen, you should know immediately that it is GitLab. Our personality is strong and consistent across product and marketing experiences.
+
+## Human and quirky
+We need to build empathy with our users, understanding their state of mind, and connect with them at a human level. Quirkiness is part of our DNA, and we should embrace it in the right moments and contexts.
+
+> TODO: Ensure these principles align well with the goals of the Marketing team
diff --git a/doc/development/ux_guide/resources.md b/doc/development/ux_guide/resources.md
new file mode 100644
index 00000000000..2f760c94414
--- /dev/null
+++ b/doc/development/ux_guide/resources.md
@@ -0,0 +1,13 @@
+# Resources
+
+## GitLab UI development kit
+
+We created a page inside GitLab where you can check commonly used html and css elements.
+
+When you run GitLab instance locally - just visit http://localhost:3000/help/ui page to see UI examples
+you can use during GitLab development.
+
+## Design repository
+
+All design files are stored in the [gitlab-design](https://gitlab.com/gitlab-org/gitlab-design)
+repository and maintained by GitLab UX designers. \ No newline at end of file
diff --git a/doc/development/ux_guide/surfaces.md b/doc/development/ux_guide/surfaces.md
new file mode 100644
index 00000000000..881d6aa4cd6
--- /dev/null
+++ b/doc/development/ux_guide/surfaces.md
@@ -0,0 +1,47 @@
+# Surfaces
+
+## Contents
+* [Header](#header)
+* [Global menu](#global-menu)
+* [Side pane](#side-pane)
+* [Content area](#content-area)
+
+---
+
+![Surfaces UX](img/surfaces-ux.png)
+
+## Global menu
+
+This menu is to navigate to pages that contain content global to GitLab.
+
+---
+
+## Header
+
+The header contains 3 main elements: Project switching and searching, user account avatar and settings, and a contextual menu that changes based on the current page.
+
+![Surfaces Header](img/surfaces-header.png)
+
+---
+
+## Side pane
+
+The side pane holds supporting information and meta data for the information in the content area.
+
+---
+
+## Content area
+
+The main content of the page. The content area can include other surfaces.
+
+### Item title bar
+
+The item title bar contains the top level information to identify the item, such as the name, id and status.
+
+![Item title](img/surfaces-contentitemtitle.png)
+
+### Item system information
+
+The system information block contains relevant system controlled information.
+
+![Item system information](img/surfaces-systeminformationblock.png)
diff --git a/doc/development/ux_guide/tips.md b/doc/development/ux_guide/tips.md
new file mode 100644
index 00000000000..8348de4f8a2
--- /dev/null
+++ b/doc/development/ux_guide/tips.md
@@ -0,0 +1,44 @@
+# Tips
+
+## Contents
+* [SVGs](#svgs)
+
+---
+
+## SVGs
+
+When exporting SVGs, be sure to follow the following guidelines:
+
+1. Convert all strokes to outlines.
+2. Use pathfinder tools to combine overlapping paths and create compound paths.
+3. SVGs that are limited to one color should be exported without a fill color so the color can be set using CSS.
+4. Ensure that exported SVGs have been run through an [SVG cleaner](https://github.com/RazrFalcon/SVGCleaner) to remove unused elements and attributes.
+
+You can open your svg in a text editor to ensure that it is clean.
+Incorrect files will look like this:
+
+```xml
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="16px" height="17px" viewBox="0 0 16 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
+ <title>Group</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Group" fill="#7E7C7C">
+ <path d="M15.1111,1 L0.8891,1 C0.3981,1 0.0001,1.446 0.0001,1.996 L0.0001,15.945 C0.0001,16.495 0.3981,16.941 0.8891,16.941 L15.1111,16.941 C15.6021,16.941 16.0001,16.495 16.0001,15.945 L16.0001,1.996 C16.0001,1.446 15.6021,1 15.1111,1 L15.1111,1 L15.1111,1 Z M14.0001,6.0002 L14.0001,14.949 L2.0001,14.949 L2.0001,6.0002 L14.0001,6.0002 Z M14.0001,4.0002 L14.0001,2.993 L2.0001,2.993 L2.0001,4.0002 L14.0001,4.0002 Z" id="Combined-Shape"></path>
+ <polygon id="Fill-11" points="3 2.0002 5 2.0002 5 0.0002 3 0.0002"></polygon>
+ <polygon id="Fill-16" points="11 2.0002 13 2.0002 13 0.0002 11 0.0002"></polygon>
+ <path d="M5.37709616,11.5511984 L6.92309616,12.7821984 C7.35112915,13.123019 7.97359761,13.0565604 8.32002627,12.6330535 L10.7740263,9.63305349 C11.1237073,9.20557058 11.0606364,8.57555475 10.6331535,8.22587373 C10.2056706,7.87619272 9.57565475,7.93926361 9.22597373,8.36674651 L6.77197373,11.3667465 L8.16890384,11.2176016 L6.62290384,9.98660159 C6.19085236,9.6425813 5.56172188,9.71394467 5.21770159,10.1459962 C4.8736813,10.5780476 4.94504467,11.2071781 5.37709616,11.5511984 L5.37709616,11.5511984 Z" id="Stroke-21"></path>
+ </g>
+ </g>
+</svg>
+```
+
+Correct file will look like this:
+
+```xml
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 17" enable-background="new 0 0 16 17"><path d="m15.1 1h-2.1v-1h-2v1h-6v-1h-2v1h-2.1c-.5 0-.9.5-.9 1v14c0 .6.4 1 .9 1h14.2c.5 0 .9-.4.9-1v-14c0-.5-.4-1-.9-1m-1.1 14h-12v-9h12v9m0-11h-12v-1h12v1"/><path d="m5.4 11.6l1.5 1.2c.4.3 1.1.3 1.4-.1l2.5-3c.3-.4.3-1.1-.1-1.4-.5-.4-1.1-.3-1.5.1l-1.8 2.2-.8-.6c-.4-.3-1.1-.3-1.4.2-.3.4-.3 1 .2 1.4"/></svg>
+```
+
+> TODO: Checkout [https://github.com/svg/svgo](https://github.com/svg/svgo)
diff --git a/doc/development/ux_guide/users.md b/doc/development/ux_guide/users.md
new file mode 100644
index 00000000000..717a902c424
--- /dev/null
+++ b/doc/development/ux_guide/users.md
@@ -0,0 +1,16 @@
+# Users
+
+> TODO: Create personas. Understand the similarities and differences across the below spectrums.
+
+## Users by organization
+
+- Enterprise
+- Medium company
+- Small company
+- Open source communities
+
+## Users by role
+
+- Admin
+- Manager
+- Developer
diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md
index 3f45a631b3a..1c549844ee1 100644
--- a/doc/gitlab-basics/create-project.md
+++ b/doc/gitlab-basics/create-project.md
@@ -14,8 +14,8 @@ There are two ways to create a new project in GitLab.
1. Fill out the information:
- 1. "Project name" is the name of your project (you can't use spaces, but you
- can use hyphens or underscores).
+ 1. "Project name" is the name of your project (you can't use special characters,
+ but you can use spaces, hyphens, underscores or even emojis).
1. The "Project description" is optional and will be shown in your project's
dashboard so others can briefly understand what your project is about.
1. Select a [visibility level](../public_access/public_access.md).
diff --git a/doc/gitlab-basics/img/create_new_group_info.png b/doc/gitlab-basics/img/create_new_group_info.png
index c8eddfd1bbb..020b4ac00d6 100644
--- a/doc/gitlab-basics/img/create_new_group_info.png
+++ b/doc/gitlab-basics/img/create_new_group_info.png
Binary files differ
diff --git a/doc/gitlab-basics/img/create_new_group_sidebar.png b/doc/gitlab-basics/img/create_new_group_sidebar.png
index 28017ee02e0..fa88d1d51c0 100644
--- a/doc/gitlab-basics/img/create_new_group_sidebar.png
+++ b/doc/gitlab-basics/img/create_new_group_sidebar.png
Binary files differ
diff --git a/doc/gitlab-basics/img/create_new_project_button.png b/doc/gitlab-basics/img/create_new_project_button.png
index e7c794d943f..a19f0e57b56 100644
--- a/doc/gitlab-basics/img/create_new_project_button.png
+++ b/doc/gitlab-basics/img/create_new_project_button.png
Binary files differ
diff --git a/doc/gitlab-basics/img/create_new_project_from_group.png b/doc/gitlab-basics/img/create_new_project_from_group.png
index 6d41d17f9ca..c35234660db 100644
--- a/doc/gitlab-basics/img/create_new_project_from_group.png
+++ b/doc/gitlab-basics/img/create_new_project_from_group.png
Binary files differ
diff --git a/doc/gitlab-basics/img/create_new_project_info.png b/doc/gitlab-basics/img/create_new_project_info.png
index 16d56f0707f..fcfbca87b91 100644
--- a/doc/gitlab-basics/img/create_new_project_info.png
+++ b/doc/gitlab-basics/img/create_new_project_info.png
Binary files differ
diff --git a/doc/gitlab-basics/img/fork_choose_namespace.png b/doc/gitlab-basics/img/fork_choose_namespace.png
index 82c9c3bd39e..4c50276d5ad 100644
--- a/doc/gitlab-basics/img/fork_choose_namespace.png
+++ b/doc/gitlab-basics/img/fork_choose_namespace.png
Binary files differ
diff --git a/doc/gitlab-basics/img/fork_new.png b/doc/gitlab-basics/img/fork_new.png
index 41885223286..fa185fdaca1 100644
--- a/doc/gitlab-basics/img/fork_new.png
+++ b/doc/gitlab-basics/img/fork_new.png
Binary files differ
diff --git a/doc/gitlab-basics/img/merge_request_new.png b/doc/gitlab-basics/img/merge_request_new.png
index 0aba5743f01..6fcd7bebada 100644
--- a/doc/gitlab-basics/img/merge_request_new.png
+++ b/doc/gitlab-basics/img/merge_request_new.png
Binary files differ
diff --git a/doc/gitlab-basics/img/merge_request_page.png b/doc/gitlab-basics/img/merge_request_page.png
index 68c3bbf9444..f6087294e22 100644
--- a/doc/gitlab-basics/img/merge_request_page.png
+++ b/doc/gitlab-basics/img/merge_request_page.png
Binary files differ
diff --git a/doc/gitlab-basics/img/merge_request_select_branch.png b/doc/gitlab-basics/img/merge_request_select_branch.png
index 516436ff6cc..9f6b93943a9 100644
--- a/doc/gitlab-basics/img/merge_request_select_branch.png
+++ b/doc/gitlab-basics/img/merge_request_select_branch.png
Binary files differ
diff --git a/doc/gitlab-basics/img/new_issue_button.png b/doc/gitlab-basics/img/new_issue_button.png
index 46b626bed65..3b113471f0c 100644
--- a/doc/gitlab-basics/img/new_issue_button.png
+++ b/doc/gitlab-basics/img/new_issue_button.png
Binary files differ
diff --git a/doc/gitlab-basics/img/new_issue_page.png b/doc/gitlab-basics/img/new_issue_page.png
index 843504130b7..ce3e60df276 100644
--- a/doc/gitlab-basics/img/new_issue_page.png
+++ b/doc/gitlab-basics/img/new_issue_page.png
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings.png b/doc/gitlab-basics/img/profile_settings.png
index f0abd478849..26df4c0a734 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.png b/doc/gitlab-basics/img/profile_settings_ssh_keys.png
index 2c9a42fe10c..8ac603a2af9 100644
--- a/doc/gitlab-basics/img/profile_settings_ssh_keys.png
+++ b/doc/gitlab-basics/img/profile_settings_ssh_keys.png
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png
index cd7add6937f..5e501ec86ef 100644
--- a/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png
+++ b/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.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 095beb02be8..6a1430d9663 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/gitlab-basics/img/profile_settings_ssh_keys_title.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png
index 4b998a7f948..89a04c17fed 100644
--- a/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png
+++ b/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png
Binary files differ
diff --git a/doc/gitlab-basics/img/project_clone_url.png b/doc/gitlab-basics/img/project_clone_url.png
index eed430e1036..bdd7d011db3 100644
--- a/doc/gitlab-basics/img/project_clone_url.png
+++ b/doc/gitlab-basics/img/project_clone_url.png
Binary files differ
diff --git a/doc/gitlab-basics/img/project_navbar.png b/doc/gitlab-basics/img/project_navbar.png
index 97cf3cd9702..be6f38ede32 100644
--- a/doc/gitlab-basics/img/project_navbar.png
+++ b/doc/gitlab-basics/img/project_navbar.png
Binary files differ
diff --git a/doc/gitlab-basics/img/select_group_dropdown.png b/doc/gitlab-basics/img/select_group_dropdown.png
index 7d8b89c2df9..68fc950304c 100644
--- a/doc/gitlab-basics/img/select_group_dropdown.png
+++ b/doc/gitlab-basics/img/select_group_dropdown.png
Binary files differ
diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md
index e8093f0b257..322680f0cf4 100644
--- a/doc/install/database_mysql.md
+++ b/doc/install/database_mysql.md
@@ -57,8 +57,15 @@ We do not recommend using MySQL due to various issues. For example, case [(in)se
After installation or upgrade, remember to run the `add_limits_mysql` Rake task:
+**Omnibus GitLab installations**
```
-bundle exec rake add_limits_mysql
+sudo gitlab-rake add_limits_mysql
+```
+
+**Installations from source**
+
+```
+bundle exec rake add_limits_mysql RAILS_ENV=production
```
The `text` type in MySQL has a different size limit than the `text` type in
diff --git a/doc/install/installation.md b/doc/install/installation.md
index b5e2640b380..2740b2982b9 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -76,7 +76,7 @@ Make sure you have the right version of Git installed
# Install Git
sudo apt-get install -y git-core
- # Make sure Git is version 2.7.4 or higher
+ # Make sure Git is version 2.8.4 or higher
git --version
Is the system packaged Git too old? Remove it and compile from source.
@@ -89,9 +89,9 @@ Is the system packaged Git too old? Remove it and compile from source.
# Download and compile from source
cd /tmp
- curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.7.4.tar.gz
- echo '7104c4f5d948a75b499a954524cb281fe30c6649d8abe20982936f75ec1f275b git-2.7.4.tar.gz' | shasum -a256 -c - && tar -xzf git-2.7.4.tar.gz
- cd git-2.7.4/
+ curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.8.4.tar.gz
+ echo '626e319f8a24fc0866167ea5f6bf3e2f38f69d6cb2e59e150f13709ca3ebf301 git-2.8.4.tar.gz' | shasum -a256 -c - && tar -xzf git-2.8.4.tar.gz
+ cd git-2.8.4/
./configure
make prefix=/usr/local all
@@ -123,9 +123,9 @@ Remove the old Ruby 1.8 if present:
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.1.tar.gz
- echo 'c39b4001f7acb4e334cb60a0f4df72d434bef711 ruby-2.3.1.tar.gz' | shasum -c - && tar xzf ruby-2.3.1.tar.gz
- cd ruby-2.3.1
+ curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
+ echo 'a8db9ce7f9110320f33b8325200e3ecfbd2b534b ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
+ cd ruby-2.3.3
./configure --disable-install-rdoc
make
sudo make install
@@ -176,16 +176,16 @@ We recommend using a PostgreSQL database. For MySQL check the
sudo -u postgres psql -d template1 -c "CREATE USER git CREATEDB;"
```
-1. Create the GitLab production database and grant all privileges on database:
+1. Create the `pg_trgm` extension (required for GitLab 8.6+):
```bash
- sudo -u postgres psql -d template1 -c "CREATE DATABASE gitlabhq_production OWNER git;"
+ sudo -u postgres psql -d template1 -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;"
```
-1. Create the `pg_trgm` extension (required for GitLab 8.6+):
+1. Create the GitLab production database and grant all privileges on database:
```bash
- sudo -u postgres psql -d template1 -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;"
+ sudo -u postgres psql -d template1 -c "CREATE DATABASE gitlabhq_production OWNER git;"
```
1. Try connecting to the new database with the new user:
@@ -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-14-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-15-stable gitlab
-**Note:** You can change `8-14-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-15-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -396,15 +396,13 @@ GitLab Shell is an SSH access and repository management software developed speci
### Install gitlab-workhorse
-GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/).
-If you are not using Linux you may have to run `gmake` instead of
-`make` below.
+GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/). The
+following command-line will install GitLab-Workhorse in `/home/git/gitlab-workhorse`
+which is the recommended location.
- cd /home/git
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git
- cd gitlab-workhorse
- sudo -u git -H git checkout v1.0.0
- sudo -u git -H make
+ cd /home/git/gitlab
+
+ sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
### Initialize Database and Activate Advanced Features
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 766a7119943..e942346e2d7 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -143,9 +143,6 @@ On a very active server (10,000 active users) the Sidekiq process can use 1GB+ o
## Supported web browsers
-- Chrome (Latest stable version)
-- Firefox (Latest released version and [latest ESR version](https://www.mozilla.org/en-US/firefox/organizations/))
-- Safari 7+ (known problem: required fields in html5 do not work)
-- Opera (Latest released version)
-- Internet Explorer (IE) 11+ but please make sure that you have the `Compatibility View` mode disabled.
-- Edge (Latest stable version)
+We support the current and the previous major release of Firefox, Chrome/Chromium, Safari and Microsoft browsers (Microsoft Edge and Internet Explorer 11).
+
+Each time a new browser version is released, we begin supporting that version and stop supporting the third most recent version.
diff --git a/doc/integration/README.md b/doc/integration/README.md
index a928b74f9b8..f8ffa6dcb7f 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -5,7 +5,7 @@ trackers and external authentication.
See the documentation below for details on how to configure these services.
-- [JIRA](jira.md) Integrate with the JIRA issue tracker
+- [JIRA](../project_services/jira.md) Integrate with the JIRA issue tracker
- [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc.
- [LDAP](ldap.md) Set up sign in via LDAP
- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure
@@ -44,11 +44,15 @@ This [resource](http://kb.kerio.com/product/kerio-connect/server-configuration/s
has all the information you need to add a certificate to the main trusted chain.
This [answer](http://superuser.com/questions/437330/how-do-you-add-a-certificate-authority-ca-to-ubuntu)
-at SuperUser also has relevant information.
+at Super User also has relevant information.
**Omnibus Trusted Chain**
-It is enough to concatenate the certificate to the main trusted certificate:
+[Install the self signed certificate or custom certificate authorities](http://docs.gitlab.com/omnibus/common_installation_problems/README.html#using-self-signed-certificate-or-custom-certificate-authorities)
+in to GitLab Omnibus.
+
+It is enough to concatenate the certificate to the main trusted certificate
+however it may be overwritten during upgrades:
```bash
cat jira.pem >> /opt/gitlab/embedded/ssl/certs/cacert.pem
diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md
index 556d71b8b76..5df6e103f42 100644
--- a/doc/integration/bitbucket.md
+++ b/doc/integration/bitbucket.md
@@ -18,8 +18,10 @@ Bitbucket.org.
## Bitbucket OmniAuth provider
> **Note:**
-Make sure to first follow the [Initial OmniAuth configuration][init-oauth]
-before proceeding with setting up the Bitbucket integration.
+GitLab 8.15 significantly simplified the way to integrate Bitbucket.org with
+GitLab. You are encouraged to upgrade your GitLab instance if you haven't done
+already. If you're using GitLab 8.14 and below, [use the previous integration
+docs][bb-old].
To enable the Bitbucket OmniAuth provider you must register your application
with Bitbucket.org. Bitbucket will generate an application ID and secret key for
@@ -44,14 +46,12 @@ you to use.
And grant at least the following permissions:
```
- Account: Email
- Repositories: Read, Admin
+ Account: Email, Read
+ Repositories: Read
+ Pull Requests: Read
+ Issues: Read
```
- >**Note:**
- It may seem a little odd to giving GitLab admin permissions to repositories,
- but this is needed in order for GitLab to be able to clone the repositories.
-
![Bitbucket OAuth settings page](img/bitbucket_oauth_settings_page.png)
1. Select **Save**.
@@ -93,7 +93,8 @@ you to use.
```yaml
- { name: 'bitbucket',
app_id: 'BITBUCKET_APP_KEY',
- app_secret: 'BITBUCKET_APP_SECRET' }
+ app_secret: 'BITBUCKET_APP_SECRET',
+ url: 'https://bitbucket.org/' }
```
---
@@ -112,100 +113,12 @@ well, the user will be returned to GitLab and will be signed in.
## Bitbucket project import
-To allow projects to be imported directly into GitLab, Bitbucket requires two
-extra setup steps compared to [GitHub](github.md) and [GitLab.com](gitlab.md).
-
-Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and
-instead requires GitLab to use SSH and identify itself using your GitLab
-server's SSH key.
-
-To be able to access repositories on Bitbucket, GitLab will automatically
-register your public key with Bitbucket as a deploy key for the repositories to
-be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa` which
-translates to `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages and to
-`/home/git/.ssh/bitbucket_rsa.pub` for installations from source.
-
----
-
-Below are the steps that will allow GitLab to be able to import your projects
-from Bitbucket.
-
-1. Make sure you [have enabled the Bitbucket OAuth support](#bitbucket-omniauth-provider).
-1. Create a new SSH key with an **empty passphrase**:
-
- ```sh
- sudo -u git -H ssh-keygen
- ```
-
- When asked to 'Enter file in which to save the key' enter:
- `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages or
- `/home/git/.ssh/bitbucket_rsa` for installations from source. The name is
- important so make sure to get it right.
-
- > **Warning:**
- This key must NOT be associated with ANY existing Bitbucket accounts. If it
- is, the import will fail with an `Access denied! Please verify you can add
- deploy keys to this repository.` error.
-
-1. Next, you need to to configure the SSH client to use your new key. Open the
- SSH configuration file of the `git` user:
-
- ```
- # For Omnibus packages
- sudo editor /var/opt/gitlab/.ssh/config
-
- # For installations from source
- sudo editor /home/git/.ssh/config
- ```
-
-1. Add a host configuration for `bitbucket.org`:
-
- ```sh
- Host bitbucket.org
- IdentityFile ~/.ssh/bitbucket_rsa
- User git
- ```
-
-1. Save the file and exit.
-1. Manually connect to `bitbucket.org` over SSH, while logged in as the `git`
- user that GitLab will use:
-
- ```sh
- sudo -u git -H ssh bitbucket.org
- ```
-
- That step is performed because GitLab needs to connect to Bitbucket over SSH,
- in order to add `bitbucket.org` to your GitLab server's known SSH hosts.
-
-1. Verify the RSA key fingerprint you'll see in the response matches the one
- in the [Bitbucket documentation][bitbucket-docs] (the specific IP address
- doesn't matter):
-
- ```sh
- The authenticity of host 'bitbucket.org (104.192.143.1)' can't be established.
- RSA key fingerprint is SHA256:zzXQOXSRBEiUtuE8AikJYKwbHaxvSc0ojez9YXaGp1A.
- Are you sure you want to continue connecting (yes/no)?
- ```
-
-1. If the fingerprint matches, type `yes` to continue connecting and have
- `bitbucket.org` be added to your known SSH hosts. After confirming you should
- see a permission denied message. If you see an authentication successful
- message you have done something wrong. The key you are using has already been
- added to a Bitbucket account and will cause the import script to fail. Ensure
- the key you are using CANNOT authenticate with Bitbucket.
-1. Restart GitLab to allow it to find the new public key.
-
-Your GitLab server is now able to connect to Bitbucket over SSH. You should be
-able to see the "Import projects from Bitbucket" option on the New Project page
-enabled.
-
-## Acknowledgemts
-
-Special thanks to the writer behind the following article:
-
-- http://stratus3d.com/blog/2015/09/06/migrating-from-bitbucket-to-local-gitlab-server/
+Once the above configuration is set up, you can use Bitbucket to sign into
+GitLab and [start importing your projects][bb-import].
[init-oauth]: omniauth.md#initial-omniauth-configuration
+[bb-import]: ../workflow/importing/import_projects_from_bitbucket.md
+[bb-old]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-14-stable/doc/integration/bitbucket.md
[bitbucket-docs]: https://confluence.atlassian.com/bitbucket/use-the-ssh-protocol-with-bitbucket-cloud-221449711.html#UsetheSSHprotocolwithBitbucketCloud-KnownhostorBitbucket%27spublickeyfingerprints
[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 8a01afd1177..479c697b933 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -48,6 +48,21 @@ GitHub will generate an application ID and secret key for you to use.
For omnibus package:
+ For GitHub.com:
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "github",
+ "app_id" => "YOUR_APP_ID",
+ "app_secret" => "YOUR_APP_SECRET",
+ "args" => { "scope" => "user:email" }
+ }
+ ]
+ ```
+
+ For GitHub Enterprise:
+
```ruby
gitlab_rails['omniauth_providers'] = [
{
@@ -86,7 +101,7 @@ 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.
+1. Save the configuration file and run `sudo gitlab-ctl reconfigure`.
1. Restart GitLab for the changes to take effect.
diff --git a/doc/integration/img/akismet_settings.png b/doc/integration/img/akismet_settings.png
index c2aa97b132e..689654bf960 100644
--- a/doc/integration/img/akismet_settings.png
+++ b/doc/integration/img/akismet_settings.png
Binary files differ
diff --git a/doc/integration/img/bitbucket_oauth_keys.png b/doc/integration/img/bitbucket_oauth_keys.png
index 3fb2f7524a3..6dd2c7d744e 100644
--- a/doc/integration/img/bitbucket_oauth_keys.png
+++ b/doc/integration/img/bitbucket_oauth_keys.png
Binary files differ
diff --git a/doc/integration/img/bitbucket_oauth_settings_page.png b/doc/integration/img/bitbucket_oauth_settings_page.png
index a3047712d8c..21ce82a6074 100644
--- a/doc/integration/img/bitbucket_oauth_settings_page.png
+++ b/doc/integration/img/bitbucket_oauth_settings_page.png
Binary files differ
diff --git a/doc/integration/img/enabled-oauth-sign-in-sources.png b/doc/integration/img/enabled-oauth-sign-in-sources.png
index b23d6dcc595..f145aeae75c 100644
--- a/doc/integration/img/enabled-oauth-sign-in-sources.png
+++ b/doc/integration/img/enabled-oauth-sign-in-sources.png
Binary files differ
diff --git a/doc/integration/img/facebook_api_keys.png b/doc/integration/img/facebook_api_keys.png
index 995845d5a69..9463ec1e7a3 100644
--- a/doc/integration/img/facebook_api_keys.png
+++ b/doc/integration/img/facebook_api_keys.png
Binary files differ
diff --git a/doc/integration/img/facebook_app_settings.png b/doc/integration/img/facebook_app_settings.png
index 1cd586ecd7c..81f38cab16e 100644
--- a/doc/integration/img/facebook_app_settings.png
+++ b/doc/integration/img/facebook_app_settings.png
Binary files differ
diff --git a/doc/integration/img/facebook_website_url.png b/doc/integration/img/facebook_website_url.png
index 10e1bd5d5a6..67d78d13951 100644
--- a/doc/integration/img/facebook_website_url.png
+++ b/doc/integration/img/facebook_website_url.png
Binary files differ
diff --git a/doc/integration/img/github_app.png b/doc/integration/img/github_app.png
index de31242679a..d6c289a1de1 100644
--- a/doc/integration/img/github_app.png
+++ b/doc/integration/img/github_app.png
Binary files differ
diff --git a/doc/integration/img/gitlab_app.png b/doc/integration/img/gitlab_app.png
index 065316fd3c7..b4958581a9b 100644
--- a/doc/integration/img/gitlab_app.png
+++ b/doc/integration/img/gitlab_app.png
Binary files differ
diff --git a/doc/integration/img/gmail_action_buttons_for_gitlab.png b/doc/integration/img/gmail_action_buttons_for_gitlab.png
index a6704139091..0e3e24d6ffc 100644
--- a/doc/integration/img/gmail_action_buttons_for_gitlab.png
+++ b/doc/integration/img/gmail_action_buttons_for_gitlab.png
Binary files differ
diff --git a/doc/integration/img/google_app.png b/doc/integration/img/google_app.png
index 08f7f714553..9fda06dabb1 100644
--- a/doc/integration/img/google_app.png
+++ b/doc/integration/img/google_app.png
Binary files differ
diff --git a/doc/integration/img/jira_add_user_to_group.png b/doc/integration/img/jira_add_user_to_group.png
deleted file mode 100644
index 0ba737bda9a..00000000000
--- a/doc/integration/img/jira_add_user_to_group.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/img/jira_create_new_group.png b/doc/integration/img/jira_create_new_group.png
deleted file mode 100644
index 0609060cb05..00000000000
--- a/doc/integration/img/jira_create_new_group.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/img/jira_create_new_group_name.png b/doc/integration/img/jira_create_new_group_name.png
deleted file mode 100644
index 53d77b17df0..00000000000
--- a/doc/integration/img/jira_create_new_group_name.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/img/jira_create_new_user.png b/doc/integration/img/jira_create_new_user.png
deleted file mode 100644
index 9eaa444ed25..00000000000
--- a/doc/integration/img/jira_create_new_user.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/img/jira_group_access.png b/doc/integration/img/jira_group_access.png
deleted file mode 100644
index 8d4657427ae..00000000000
--- a/doc/integration/img/jira_group_access.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/img/jira_issue_reference.png b/doc/integration/img/jira_issue_reference.png
deleted file mode 100644
index 1a2d9f04a6c..00000000000
--- a/doc/integration/img/jira_issue_reference.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/img/jira_merge_request_close.png b/doc/integration/img/jira_merge_request_close.png
deleted file mode 100644
index b8f6058a514..00000000000
--- a/doc/integration/img/jira_merge_request_close.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/img/jira_project_name.png b/doc/integration/img/jira_project_name.png
deleted file mode 100644
index e785ec6140d..00000000000
--- a/doc/integration/img/jira_project_name.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/img/jira_service.png b/doc/integration/img/jira_service.png
deleted file mode 100644
index 13aefce6f84..00000000000
--- a/doc/integration/img/jira_service.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/img/jira_service_close_issue.png b/doc/integration/img/jira_service_close_issue.png
deleted file mode 100644
index eed69e80d2c..00000000000
--- a/doc/integration/img/jira_service_close_issue.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/img/jira_service_page.png b/doc/integration/img/jira_service_page.png
deleted file mode 100644
index 0cc160bebe2..00000000000
--- a/doc/integration/img/jira_service_page.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/img/jira_user_management_link.png b/doc/integration/img/jira_user_management_link.png
deleted file mode 100644
index 5f002b59bac..00000000000
--- a/doc/integration/img/jira_user_management_link.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/img/jira_workflow_screenshot.png b/doc/integration/img/jira_workflow_screenshot.png
deleted file mode 100644
index 937a50a77d9..00000000000
--- a/doc/integration/img/jira_workflow_screenshot.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/img/oauth_provider_admin_application.png b/doc/integration/img/oauth_provider_admin_application.png
index fc5f7596fcc..c8ecce129c8 100644
--- a/doc/integration/img/oauth_provider_admin_application.png
+++ b/doc/integration/img/oauth_provider_admin_application.png
Binary files differ
diff --git a/doc/integration/img/oauth_provider_application_form.png b/doc/integration/img/oauth_provider_application_form.png
index 606ab3e3467..954681e054e 100644
--- a/doc/integration/img/oauth_provider_application_form.png
+++ b/doc/integration/img/oauth_provider_application_form.png
Binary files differ
diff --git a/doc/integration/img/oauth_provider_application_id_secret.png b/doc/integration/img/oauth_provider_application_id_secret.png
index cbedcef8376..65cca5f1e1b 100644
--- a/doc/integration/img/oauth_provider_application_id_secret.png
+++ b/doc/integration/img/oauth_provider_application_id_secret.png
Binary files differ
diff --git a/doc/integration/img/oauth_provider_authorized_application.png b/doc/integration/img/oauth_provider_authorized_application.png
index 6a2ea09073c..ed99db3476d 100644
--- a/doc/integration/img/oauth_provider_authorized_application.png
+++ b/doc/integration/img/oauth_provider_authorized_application.png
Binary files differ
diff --git a/doc/integration/img/oauth_provider_user_wide_applications.png b/doc/integration/img/oauth_provider_user_wide_applications.png
index 0c7b095a2dd..9cc12555574 100644
--- a/doc/integration/img/oauth_provider_user_wide_applications.png
+++ b/doc/integration/img/oauth_provider_user_wide_applications.png
Binary files differ
diff --git a/doc/integration/img/spam_log.png b/doc/integration/img/spam_log.png
index 8d574448690..43e267daff4 100644
--- a/doc/integration/img/spam_log.png
+++ b/doc/integration/img/spam_log.png
Binary files differ
diff --git a/doc/integration/img/submit_issue.png b/doc/integration/img/submit_issue.png
index 5c7896a7eec..8accb78faf3 100644
--- a/doc/integration/img/submit_issue.png
+++ b/doc/integration/img/submit_issue.png
Binary files differ
diff --git a/doc/integration/img/twitter_app_api_keys.png b/doc/integration/img/twitter_app_api_keys.png
index 15b29ac7d16..34e3c3ba001 100644
--- a/doc/integration/img/twitter_app_api_keys.png
+++ b/doc/integration/img/twitter_app_api_keys.png
Binary files differ
diff --git a/doc/integration/img/twitter_app_details.png b/doc/integration/img/twitter_app_details.png
index 323112a88bb..b53f4eb3202 100644
--- a/doc/integration/img/twitter_app_details.png
+++ b/doc/integration/img/twitter_app_details.png
Binary files differ
diff --git a/doc/integration/jira.md b/doc/integration/jira.md
index 2e31fd994de..e2f136bcc35 100644
--- a/doc/integration/jira.md
+++ b/doc/integration/jira.md
@@ -1,197 +1,3 @@
# GitLab JIRA integration
-GitLab can be configured to interact with JIRA. Configuration happens via
-user name and password. Connecting to a JIRA server via CAS is not possible.
-
-Each project can be configured to connect to a different JIRA instance, see the
-[configuration](#configuration) section. If you have one JIRA instance you can
-pre-fill the settings page with a default template. To configure the template
-see the [Services Templates][services-templates] document.
-
-Once the project is connected to JIRA, you can reference and close the issues
-in JIRA directly from GitLab.
-
-## Table of Contents
-* [Referencing JIRA Issues from GitLab](#referencing-JIRA-issues)
-* [Closing JIRA Issues from GitLab](#closing-JIRA-issues)
-* [Configuration](#configuration)
-
-### Referencing JIRA Issues
-
-When GitLab project has JIRA issue tracker configured and enabled, mentioning
-JIRA issue in GitLab will automatically add a comment in JIRA issue with the
-link back to GitLab. This means that in comments in merge requests and commits
-referencing an issue, eg. `PROJECT-7`, will add a comment in JIRA issue in the
-format:
-
-```
- USER mentioned this issue in RESOURCE_NAME of [PROJECT_NAME|LINK_TO_COMMENT]:
- ENTITY_TITLE
-```
-
-* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab.
-* `LINK_TO_THE_COMMENT` Link to the origin of mention with a name of the entity where JIRA issue was mentioned.
-* `RESOURCE_NAME` Kind of resource which referenced the issue. Can be a commit or merge request.
-* `PROJECT_NAME` GitLab project name.
-* `ENTITY_TITLE` Merge request title or commit message first line.
-
-![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png)
-
----
-
-### Closing JIRA Issues
-
-JIRA issues can be closed directly from GitLab by using trigger words, eg.
-`Resolves PROJECT-1`, `Closes PROJECT-1` or `Fixes PROJECT-1`, in commits and
-merge requests. When a commit which contains the trigger word in the commit
-message is pushed, GitLab will add a comment in the mentioned JIRA issue.
-
-For example, for project named `PROJECT` in JIRA, we implemented a new feature
-and created a merge request in GitLab.
-
-This feature was requested in JIRA issue `PROJECT-7`. Merge request in GitLab
-contains the improvement and in merge request description we say that this
-merge request `Closes PROJECT-7` issue.
-
-Once this merge request is merged, the JIRA issue will be automatically closed
-with a link to the commit that resolved the issue.
-
-![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png)
-
----
-
-![The GitLab integration user leaves a comment on JIRA](img/jira_service_close_issue.png)
-
----
-
-## Configuration
-
-### Configuring JIRA
-
-We need to create a user in JIRA which will have access to all projects that
-need to integrate with GitLab. Login to your JIRA instance as admin and under
-Administration go to User Management and create a new user.
-
-As an example, we'll create a user named `gitlab` and add it to `JIRA-developers`
-group.
-
-**It is important that the user `GitLab` has write-access to projects in JIRA**
-
-We have split this stage in steps so it is easier to follow.
-
----
-
-1. Login to your JIRA instance as an administrator and under **Administration**
- go to **User Management** to create a new user.
-
- ![JIRA user management link](img/jira_user_management_link.png)
-
- ---
-
-1. The next step is to create a new user (e.g., `gitlab`) who has write access
- to projects in JIRA. Enter the user's name and a _valid_ e-mail address
- since JIRA sends a verification e-mail to set-up the password.
- _**Note:** JIRA creates the username automatically by using the e-mail
- prefix. You can change it later if you want._
-
- ![JIRA create new user](img/jira_create_new_user.png)
-
- ---
-
-1. Now, let's create a `gitlab-developers` group which will have write access
- to projects in JIRA. Go to the **Groups** tab and select **Create group**.
-
- ![JIRA create new user](img/jira_create_new_group.png)
-
- ---
-
- Give it an optional description and hit **Create group**.
-
- ![jira create new group](img/jira_create_new_group_name.png)
-
- ---
-
-1. Give the newly-created group write access by going to
- **Application access > View configuration** and adding the `gitlab-developers`
- group to JIRA Core.
-
- ![JIRA group access](img/jira_group_access.png)
-
- ---
-
-1. Add the `gitlab` user to the `gitlab-developers` group by going to
- **Users > GitLab user > Add group** and selecting the `gitlab-developers`
- group from the dropdown menu. Notice that the group says _Access_ which is
- what we aim for.
-
- ![JIRA add user to group](img/jira_add_user_to_group.png)
-
----
-
-The JIRA configuration is over. Write down the new JIRA username and its
-password as they will be needed when configuring GitLab in the next section.
-
-### Configuring GitLab
-
-JIRA configuration in GitLab is done via a project's **Services**.
-
-#### GitLab 8.13.0 with JIRA v1000.x
-
-To enable JIRA integration in a project, navigate to the project's
-and open the context menu clicking on the top right gear icon, then go to
-**Services > JIRA**.
-
-Fill in the required details on the page as described in the table below.
-
-| Field | Description |
-| ----- | ----------- |
-| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. Ex. https://JIRA.example.com |
-| `Project key` | The short, all capital letter identifier for your JIRA project. |
-| `Username` | The user name created in [configuring JIRA step](#configuring-JIRA). |
-| `Password` |The password of the user created in [configuring JIRA step](#configuring-JIRA). |
-| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). |
-
-After saving the configuration, your GitLab project will be able to interact
-with the linked JIRA project.
-
-![JIRA service page](img/jira_service_page.png)
-
----
-
-#### GitLab 6.x-7.7 with JIRA v6.x
-
-_**Note:** GitLab versions 8.13.0 and up contain various integration improvements.
-We strongly recommend upgrading._
-
-In `gitlab.yml` enable the JIRA issue tracker section by
-[uncommenting these lines][JIRA-gitlab-yml]. This will make sure that all
-issues within GitLab are pointing to the JIRA issue tracker.
-
-After you set this, you will be able to close issues in JIRA by a commit in
-GitLab.
-
-Go to your project's **Settings** page and fill in the project name for the
-JIRA project:
-
-![Set the JIRA project name in GitLab to 'NEW'](img/jira_project_name.png)
-
----
-
-You can also enable the JIRA service that will allow you to interact with JIRA
-issues. Go to the **Settings > Services > JIRA** and:
-
-1. Tick the active check box to enable the service
-1. Supply the URL to JIRA server, for example http://JIRA.example.com
-1. Supply the username of a user we created under `Configuring JIRA` section,
- for example `gitlab`
-1. Supply the password of the user
-1. Optional: supply the JIRA API version, default is version `2`
-1. Optional: supply the JIRA issue transition ID (issue transition to closed).
- This is dependent on JIRA settings, default is `2`
-1. Hit save
-
-
-![JIRA services page](img/jira_service.png)
-
-[services-templates]: ../project_services/services_templates.md
-[JIRA-gitlab-yml]: https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115
+This document was moved to [project_services/jira](../project_services/jira.md).
diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md
index 5210ce0de9a..696c1011eeb 100644
--- a/doc/integration/shibboleth.md
+++ b/doc/integration/shibboleth.md
@@ -2,7 +2,7 @@
This documentation is for enabling shibboleth with omnibus-gitlab package.
-In order to enable Shibboleth support in gitlab we need to use Apache instead of Nginx (It may be possible to use Nginx, however this is difficult to configure using the bundled NIGNX provided in the omnibus-gitlab package). Apache uses mod_shib2 module for shibboleth authentication and can pass attributes as headers to omniauth-shibboleth provider.
+In order to enable Shibboleth support in gitlab we need to use Apache instead of Nginx (It may be possible to use Nginx, however this is difficult to configure using the bundled Nginx provided in the omnibus-gitlab package). Apache uses mod_shib2 module for shibboleth authentication and can pass attributes as headers to omniauth-shibboleth provider.
To enable the Shibboleth OmniAuth provider you must:
@@ -10,7 +10,7 @@ To enable the Shibboleth OmniAuth provider you must:
1. Configure Apache shibboleth module. Installation and configuration of module it self is out of scope of this document.
Check https://wiki.shibboleth.net/ for more info.
-1. You can find Apache config in gitlab-recipes (https://github.com/gitlabhq/gitlab-recipes/blob/master/web-server/apache/gitlab-ssl.conf)
+1. You can find Apache config in gitlab-recipes (https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache)
Following changes are needed to enable shibboleth:
diff --git a/doc/intro/README.md b/doc/intro/README.md
index 1790b2b761f..1df6a52ce8a 100644
--- a/doc/intro/README.md
+++ b/doc/intro/README.md
@@ -23,7 +23,7 @@ Create merge requests and review code.
- [Fork a project and contribute to it](../workflow/forking_workflow.md)
- [Create a new merge request](../gitlab-basics/add-merge-request.md)
- [Automatically close issues from merge requests](../user/project/issues/automatic_issue_closing.md)
-- [Automatically merge when your builds succeed](../user/project/merge_requests/merge_when_build_succeeds.md)
+- [Automatically merge when pipeline succeeds](../user/project/merge_requests/merge_when_pipeline_succeeds.md)
- [Revert any commit](../user/project/merge_requests/revert_changes.md)
- [Cherry-pick any commit](../user/project/merge_requests/cherry_pick_changes.md)
diff --git a/doc/monitoring/performance/img/grafana_dashboard_dropdown.png b/doc/monitoring/performance/img/grafana_dashboard_dropdown.png
index 7e34fad71ce..51eef90068d 100644
--- a/doc/monitoring/performance/img/grafana_dashboard_dropdown.png
+++ b/doc/monitoring/performance/img/grafana_dashboard_dropdown.png
Binary files differ
diff --git a/doc/monitoring/performance/img/grafana_dashboard_import.png b/doc/monitoring/performance/img/grafana_dashboard_import.png
index f97624365c7..7761ea00522 100644
--- a/doc/monitoring/performance/img/grafana_dashboard_import.png
+++ b/doc/monitoring/performance/img/grafana_dashboard_import.png
Binary files differ
diff --git a/doc/monitoring/performance/img/grafana_data_source_configuration.png b/doc/monitoring/performance/img/grafana_data_source_configuration.png
index 7d50e4c88c2..3e749eb8f9d 100644
--- a/doc/monitoring/performance/img/grafana_data_source_configuration.png
+++ b/doc/monitoring/performance/img/grafana_data_source_configuration.png
Binary files differ
diff --git a/doc/monitoring/performance/img/grafana_data_source_empty.png b/doc/monitoring/performance/img/grafana_data_source_empty.png
index aa39a53acae..33fcaaaef64 100644
--- a/doc/monitoring/performance/img/grafana_data_source_empty.png
+++ b/doc/monitoring/performance/img/grafana_data_source_empty.png
Binary files differ
diff --git a/doc/monitoring/performance/img/grafana_save_icon.png b/doc/monitoring/performance/img/grafana_save_icon.png
index c740e33cd1c..c18f2147e9d 100644
--- a/doc/monitoring/performance/img/grafana_save_icon.png
+++ b/doc/monitoring/performance/img/grafana_save_icon.png
Binary files differ
diff --git a/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png b/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png
index e6ed45a0386..d96a18ebc04 100644
--- a/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png
+++ b/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png
Binary files differ
diff --git a/doc/profile/2fa_u2f_authenticate.png b/doc/profile/2fa_u2f_authenticate.png
index b9138ff60db..b224ab14195 100644
--- a/doc/profile/2fa_u2f_authenticate.png
+++ b/doc/profile/2fa_u2f_authenticate.png
Binary files differ
diff --git a/doc/profile/2fa_u2f_register.png b/doc/profile/2fa_u2f_register.png
index 15b3683ef73..1cc142aa851 100644
--- a/doc/profile/2fa_u2f_register.png
+++ b/doc/profile/2fa_u2f_register.png
Binary files differ
diff --git a/doc/project_services/img/builds_emails_service.png b/doc/project_services/img/builds_emails_service.png
index 440728795be..9dbbed03833 100644
--- a/doc/project_services/img/builds_emails_service.png
+++ b/doc/project_services/img/builds_emails_service.png
Binary files differ
diff --git a/doc/project_services/img/emails_on_push_service.png b/doc/project_services/img/emails_on_push_service.png
index cd6f79ad1eb..df301aa1eeb 100644
--- a/doc/project_services/img/emails_on_push_service.png
+++ b/doc/project_services/img/emails_on_push_service.png
Binary files differ
diff --git a/doc/project_services/img/jira_add_user_to_group.png b/doc/project_services/img/jira_add_user_to_group.png
new file mode 100644
index 00000000000..27dac49260c
--- /dev/null
+++ b/doc/project_services/img/jira_add_user_to_group.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_group.png b/doc/project_services/img/jira_create_new_group.png
new file mode 100644
index 00000000000..06c4e84fc61
--- /dev/null
+++ b/doc/project_services/img/jira_create_new_group.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_group_name.png b/doc/project_services/img/jira_create_new_group_name.png
new file mode 100644
index 00000000000..bfc0dc6b2e9
--- /dev/null
+++ b/doc/project_services/img/jira_create_new_group_name.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_user.png b/doc/project_services/img/jira_create_new_user.png
new file mode 100644
index 00000000000..e9c03ed770d
--- /dev/null
+++ b/doc/project_services/img/jira_create_new_user.png
Binary files differ
diff --git a/doc/project_services/img/jira_group_access.png b/doc/project_services/img/jira_group_access.png
new file mode 100644
index 00000000000..9d64cc57269
--- /dev/null
+++ b/doc/project_services/img/jira_group_access.png
Binary files differ
diff --git a/doc/project_services/img/jira_issue_reference.png b/doc/project_services/img/jira_issue_reference.png
new file mode 100644
index 00000000000..72c81460df7
--- /dev/null
+++ b/doc/project_services/img/jira_issue_reference.png
Binary files differ
diff --git a/doc/project_services/img/jira_merge_request_close.png b/doc/project_services/img/jira_merge_request_close.png
new file mode 100644
index 00000000000..0f82ceba557
--- /dev/null
+++ b/doc/project_services/img/jira_merge_request_close.png
Binary files differ
diff --git a/doc/project_services/img/jira_project_name.png b/doc/project_services/img/jira_project_name.png
new file mode 100644
index 00000000000..8540a427461
--- /dev/null
+++ b/doc/project_services/img/jira_project_name.png
Binary files differ
diff --git a/doc/project_services/img/jira_service.png b/doc/project_services/img/jira_service.png
new file mode 100644
index 00000000000..8e073b84ff9
--- /dev/null
+++ b/doc/project_services/img/jira_service.png
Binary files differ
diff --git a/doc/project_services/img/jira_service_close_comment.png b/doc/project_services/img/jira_service_close_comment.png
new file mode 100644
index 00000000000..bb9cd7e3d13
--- /dev/null
+++ b/doc/project_services/img/jira_service_close_comment.png
Binary files differ
diff --git a/doc/project_services/img/jira_service_close_issue.png b/doc/project_services/img/jira_service_close_issue.png
new file mode 100644
index 00000000000..c85b1d1dd97
--- /dev/null
+++ b/doc/project_services/img/jira_service_close_issue.png
Binary files differ
diff --git a/doc/project_services/img/jira_service_page.png b/doc/project_services/img/jira_service_page.png
new file mode 100644
index 00000000000..c74351b57b8
--- /dev/null
+++ b/doc/project_services/img/jira_service_page.png
Binary files differ
diff --git a/doc/project_services/img/jira_user_management_link.png b/doc/project_services/img/jira_user_management_link.png
new file mode 100644
index 00000000000..f81c5b5fc87
--- /dev/null
+++ b/doc/project_services/img/jira_user_management_link.png
Binary files differ
diff --git a/doc/project_services/img/jira_workflow_screenshot.png b/doc/project_services/img/jira_workflow_screenshot.png
new file mode 100644
index 00000000000..e62fb202613
--- /dev/null
+++ b/doc/project_services/img/jira_workflow_screenshot.png
Binary files differ
diff --git a/doc/project_services/img/kubernetes_configuration.png b/doc/project_services/img/kubernetes_configuration.png
new file mode 100644
index 00000000000..349a2dc8456
--- /dev/null
+++ b/doc/project_services/img/kubernetes_configuration.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_add_slash_command.png b/doc/project_services/img/mattermost_add_slash_command.png
new file mode 100644
index 00000000000..7759efa183c
--- /dev/null
+++ b/doc/project_services/img/mattermost_add_slash_command.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_bot_auth.png b/doc/project_services/img/mattermost_bot_auth.png
new file mode 100644
index 00000000000..830b7849f3d
--- /dev/null
+++ b/doc/project_services/img/mattermost_bot_auth.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_bot_available_commands.png b/doc/project_services/img/mattermost_bot_available_commands.png
new file mode 100644
index 00000000000..b51798cf10d
--- /dev/null
+++ b/doc/project_services/img/mattermost_bot_available_commands.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_config_help.png b/doc/project_services/img/mattermost_config_help.png
new file mode 100644
index 00000000000..a62e4b792f9
--- /dev/null
+++ b/doc/project_services/img/mattermost_config_help.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_configuration.png b/doc/project_services/img/mattermost_configuration.png
new file mode 100644
index 00000000000..3c5ff5ee317
--- /dev/null
+++ b/doc/project_services/img/mattermost_configuration.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_console_integrations.png b/doc/project_services/img/mattermost_console_integrations.png
new file mode 100644
index 00000000000..92a30da5be0
--- /dev/null
+++ b/doc/project_services/img/mattermost_console_integrations.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_gitlab_token.png b/doc/project_services/img/mattermost_gitlab_token.png
new file mode 100644
index 00000000000..257018914d2
--- /dev/null
+++ b/doc/project_services/img/mattermost_gitlab_token.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_goto_console.png b/doc/project_services/img/mattermost_goto_console.png
new file mode 100644
index 00000000000..3354c2a24b4
--- /dev/null
+++ b/doc/project_services/img/mattermost_goto_console.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_slash_command_configuration.png b/doc/project_services/img/mattermost_slash_command_configuration.png
new file mode 100644
index 00000000000..12766ab2b34
--- /dev/null
+++ b/doc/project_services/img/mattermost_slash_command_configuration.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_slash_command_token.png b/doc/project_services/img/mattermost_slash_command_token.png
new file mode 100644
index 00000000000..c38f37c203c
--- /dev/null
+++ b/doc/project_services/img/mattermost_slash_command_token.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_team_integrations.png b/doc/project_services/img/mattermost_team_integrations.png
new file mode 100644
index 00000000000..69d4a231e5a
--- /dev/null
+++ b/doc/project_services/img/mattermost_team_integrations.png
Binary files differ
diff --git a/doc/project_services/img/redmine_configuration.png b/doc/project_services/img/redmine_configuration.png
index e9d8c0d2da8..7b6dd271401 100644
--- a/doc/project_services/img/redmine_configuration.png
+++ b/doc/project_services/img/redmine_configuration.png
Binary files differ
diff --git a/doc/project_services/img/services_templates_redmine_example.png b/doc/project_services/img/services_templates_redmine_example.png
index 77c2b98e5d0..50d20510daf 100644
--- a/doc/project_services/img/services_templates_redmine_example.png
+++ b/doc/project_services/img/services_templates_redmine_example.png
Binary files differ
diff --git a/doc/project_services/img/slack_configuration.png b/doc/project_services/img/slack_configuration.png
index b8de8a56db7..fc8e58e686b 100644
--- a/doc/project_services/img/slack_configuration.png
+++ b/doc/project_services/img/slack_configuration.png
Binary files differ
diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md
index 2ea1c58cb31..390066c9989 100644
--- a/doc/project_services/jira.md
+++ b/doc/project_services/jira.md
@@ -1 +1,208 @@
-GitLab JIRA integration documentation has moved to [here](../integration/jira.md).
+# 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
diff --git a/doc/project_services/kubernetes.md b/doc/project_services/kubernetes.md
new file mode 100644
index 00000000000..fda364b864e
--- /dev/null
+++ b/doc/project_services/kubernetes.md
@@ -0,0 +1,49 @@
+# 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
diff --git a/doc/project_services/mattermost.md b/doc/project_services/mattermost.md
new file mode 100644
index 00000000000..fbc7dfeee6d
--- /dev/null
+++ b/doc/project_services/mattermost.md
@@ -0,0 +1,45 @@
+# 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)
diff --git a/doc/project_services/mattermost_slash_commands.md b/doc/project_services/mattermost_slash_commands.md
new file mode 100644
index 00000000000..1a7c13a29b4
--- /dev/null
+++ b/doc/project_services/mattermost_slash_commands.md
@@ -0,0 +1,157 @@
+# 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].
+
+## 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
diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md
index 8116a1ce976..0f398874b8f 100644
--- a/doc/project_services/project_services.md
+++ b/doc/project_services/project_services.md
@@ -40,12 +40,15 @@ further configuration instructions and details. Contributions are welcome.
| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities |
| [HipChat](hipchat.md) | Private group chat and IM |
| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
-| [JIRA](../integration/jira.md) | JIRA issue tracker |
+| [JIRA](jira.md) | JIRA issue tracker |
| JetBrains TeamCity CI | A continuous integration and build server |
+| [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 |
| 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 |
-| [Slack](slack.md) | A team communication tool for the 21st century |
## Services Templates
diff --git a/doc/project_services/slack.md b/doc/project_services/slack.md
index 3cfe77c9f85..0b682b43810 100644
--- a/doc/project_services/slack.md
+++ b/doc/project_services/slack.md
@@ -1,4 +1,4 @@
-# Slack Service
+# Slack Notifications Service
## On Slack
@@ -15,7 +15,7 @@ Slack:
After you set up Slack, it's time to set up GitLab.
-Go to your project's **Settings > Services > Slack** and you will see a
+Go to your project's **Settings > Services > Slack Notifications** and you will see a
checkbox with the following events that can be triggered:
- Push
diff --git a/doc/raketasks/backup_hrz.png b/doc/raketasks/backup_hrz.png
index 287587609a1..c9595b236ee 100644
--- a/doc/raketasks/backup_hrz.png
+++ b/doc/raketasks/backup_hrz.png
Binary files differ
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 0ad84705cfd..f42bb6a81a2 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -32,7 +32,7 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
If you are running GitLab within a Docker container, you can run the backup from the host:
```
-docker -t exec <container name> gitlab-rake gitlab:backup:create
+docker exec -t <container name> gitlab-rake gitlab:backup:create
```
You can specify that portions of the application data be skipped using the
@@ -88,7 +88,7 @@ It uses the [Fog library](http://fog.io/) to perform the upload.
In the example below we use Amazon S3 for storage, but Fog also lets you use
[other storage providers](http://fog.io/storage/). GitLab
[imports cloud drivers](https://gitlab.com/gitlab-org/gitlab-ce/blob/30f5b9a5b711b46f1065baf755e413ceced5646b/Gemfile#L88)
-for AWS, Azure, Google, OpenStack Swift and Rackspace as well. A local driver is
+for AWS, OpenStack Swift and Rackspace as well. A local driver is
[also available](#uploading-to-locally-mounted-shares).
For omnibus packages:
@@ -353,7 +353,7 @@ restore:
```shell
# This command will overwrite the contents of your GitLab database!
-sudo gitlab-rake gitlab:backup:restore BACKUP=1393513186
+sudo gitlab-rake gitlab:backup:restore BACKUP=1393513186_2014_02_27
```
Restart and check GitLab:
diff --git a/doc/raketasks/check.md b/doc/raketasks/check.md
index 3ff3fee6a40..f7f6a40cd04 100644
--- a/doc/raketasks/check.md
+++ b/doc/raketasks/check.md
@@ -1,63 +1,3 @@
# Check Rake Tasks
-## Repository Integrity
-
-Even though Git is very resilient and tries to prevent data integrity issues,
-there are times when things go wrong. The following Rake tasks intend to
-help GitLab administrators diagnose problem repositories so they can be fixed.
-
-There are 3 things that are checked to determine integrity.
-
-1. Git repository file system check ([git fsck](https://git-scm.com/docs/git-fsck)).
- This step verifies the connectivity and validity of objects in the repository.
-1. Check for `config.lock` in the repository directory.
-1. Check for any branch/references lock files in `refs/heads`.
-
-It's important to note that the existence of `config.lock` or reference locks
-alone do not necessarily indicate a problem. Lock files are routinely created
-and removed as Git and GitLab perform operations on the repository. They serve
-to prevent data integrity issues. However, if a Git operation is interrupted these
-locks may not be cleaned up properly.
-
-The following symptoms may indicate a problem with repository integrity. If users
-experience these symptoms you may use the rake tasks described below to determine
-exactly which repositories are causing the trouble.
-
-- Receiving an error when trying to push code - `remote: error: cannot lock ref`
-- A 500 error when viewing the GitLab dashboard or when accessing a specific project.
-
-### Check all GitLab repositories
-
-This task loops through all repositories on the GitLab server and runs the
-3 integrity checks described previously.
-
-```
-# omnibus-gitlab
-sudo gitlab-rake gitlab:repo:check
-
-# installation from source
-bundle exec rake gitlab:repo:check RAILS_ENV=production
-```
-
-### Check repositories for a specific user
-
-This task checks all repositories that a specific user has access to. This is important
-because sometimes you know which user is experiencing trouble but you don't know
-which project might be the cause.
-
-If the rake task is executed without brackets at the end, you will be prompted
-to enter a username.
-
-```bash
-# omnibus-gitlab
-sudo gitlab-rake gitlab:user:check_repos
-sudo gitlab-rake gitlab:user:check_repos[<username>]
-
-# installation from source
-bundle exec rake gitlab:user:check_repos RAILS_ENV=production
-bundle exec rake gitlab:user:check_repos[<username>] RAILS_ENV=production
-```
-
-Example output:
-
-![gitlab:user:check_repos output](check_repos_output.png)
+This document was moved to [administration/raketasks/check](../administration/raketasks/check.md).
diff --git a/doc/raketasks/check_repos_output.png b/doc/raketasks/check_repos_output.png
deleted file mode 100644
index 1f632566b00..00000000000
--- a/doc/raketasks/check_repos_output.png
+++ /dev/null
Binary files differ
diff --git a/doc/security/img/two_factor_authentication_settings.png b/doc/security/img/two_factor_authentication_settings.png
index 6af5feabb13..6d89be1eb04 100644
--- a/doc/security/img/two_factor_authentication_settings.png
+++ b/doc/security/img/two_factor_authentication_settings.png
Binary files differ
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index d6a0979f6ec..9803937fcf9 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -1,75 +1,141 @@
# SSH
-## SSH keys
+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.
-An SSH key allows you to establish a secure connection between your
-computer and GitLab. Before generating an SSH key in your shell, check if your system
-already has one by running the following command:
+## Locating an existing SSH key pair
+
+Before generating a new SSH key check if your system already has one
+at the default location by opening a shell, or Command Prompt on Windows,
+and running the following command:
+
+**Windows Command Prompt:**
-**Windows Command Line:**
```bash
type %userprofile%\.ssh\id_rsa.pub
```
-**GNU/Linux/Mac/PowerShell:**
+
+**GNU/Linux / macOS / PowerShell:**
+
```bash
cat ~/.ssh/id_rsa.pub
```
-If you see a long string starting with `ssh-rsa`, you can skip the `ssh-keygen` step.
+If you see a string starting with `ssh-rsa` you already have an SSH key pair
+and you can skip the next step **Generating a new SSH key pair**
+and continue onto **Copying your public SSH key to the clipboard**.
+If you don't see the string or would like to generate a SSH key pair with a
+custom name continue onto the next step.
-To generate a new SSH key, use the following command:
-```bash
-ssh-keygen -t rsa -C "$your_email"
-```
-This command will prompt you for a location and filename to store the key
-pair and for a password. When prompted for the location and filename, just
-press enter to use the default. If you use a different name, the key will not
-be used automatically.
+## Generating a new SSH key pair
-Note: It is a best practice to use a password for an SSH key, but it is not
-required and you can skip creating a password by pressing enter.
+1. To generate a new SSH key, use the following command:
-If you want to change the password of your key later, you can use the following
-command: `ssh-keygen -p <keyname>`
+ **GNU/Linux / macOS:**
-Use the command below to show your public key:
+ ```bash
+ ssh-keygen -t rsa -C "GitLab" -b 4096
+ ```
-**Windows Command Line:**
-```bash
-type %userprofile%\.ssh\id_rsa.pub
-```
-**GNU/Linux/Mac/PowerShell:**
-```bash
-cat ~/.ssh/id_rsa.pub
-```
+ **Windows:**
-Copy-paste the key to the 'My SSH Keys' section under the 'SSH' tab in your
-user profile. Please copy the complete key starting with `ssh-rsa` and ending
-with your username and host.
+ On Windows you will need to download
+ [PuttyGen](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html)
+ and follow this [documentation article][winputty] to generate a SSH key pair.
-To copy your public key to the clipboard, use the code below. Depending on your
-OS you'll need to use a different command:
+1. Next, you will be prompted to input a file path to save your key pair to.
-**Windows Command Line:**
-```bash
-type %userprofile%\.ssh\id_rsa.pub | clip
-```
+ If you don't already have an SSH key pair use the suggested path by pressing
+ enter. Using the suggested path will allow your SSH client
+ to automatically use the key pair with no additional configuration.
-**Windows PowerShell:**
-```bash
-cat ~/.ssh/id_rsa.pub | clip
-```
+ If you already have a key pair with the suggested file path, you will need
+ to input a new file path and declare what host this key pair will be used
+ for in your `.ssh/config` file, see **Working with non-default SSH key pair paths**
+ for more information.
+
+1. Once you have input a file path you will be prompted to input a password to
+ secure your SSH key pair. It is a best practice to use a password for an SSH
+ key pair, but it is not required and you can skip creating a password by
+ pressing enter.
+
+ >**Note:**
+ If you want to change the password of your key, you can use `ssh-keygen -p <keyname>`.
+
+1. The next step is to copy the public key as we will need it afterwards.
+
+ To copy your public key to the clipboard, use the appropriate code for your
+ operating system below:
+
+ **macOS:**
+
+ ```bash
+ pbcopy < ~/.ssh/id_rsa.pub
+ ```
+
+ **GNU/Linux (requires the xclip package):**
+
+ ```bash
+ xclip -sel clip < ~/.ssh/id_rsa.pub
+ ```
+
+ **Windows Command Line:**
+
+ ```bash
+ type %userprofile%\.ssh\id_rsa.pub | clip
+ ```
+
+ **Windows PowerShell:**
+
+ ```bash
+ cat ~/.ssh/id_rsa.pub | clip
+ ```
+
+1. The final step is to add your public SSH key to GitLab.
+
+ Navigate to the 'SSH Keys' tab in you 'Profile Settings'.
+ Paste your key in the 'Key' section and give it a relevant 'Title'.
+ Use an identifiable title like 'Work Laptop - Windows 7' or
+ 'Home MacBook Pro 15'.
+
+ If you manually copied your public SSH key make sure you copied the entire
+ key starting with `ssh-rsa` and ending with your email.
+
+## Working with non-default SSH key pair paths
+
+If you used a non-default file path for your GitLab SSH key pair,
+you must configure your SSH client to find your GitLab SSH private key
+for connections to your GitLab server (perhaps gitlab.com).
+
+For OpenSSH clients this is configured in the `~/.ssh/config` file.
+Below are two example host configurations using their own key:
-**Mac:**
-```bash
-pbcopy < ~/.ssh/id_rsa.pub
```
+# GitLab.com server
+Host gitlab.com
+RSAAuthentication yes
+IdentityFile ~/.ssh/config/private-key-filename-01
-**GNU/Linux (requires xclip):**
-```bash
-xclip -sel clip < ~/.ssh/id_rsa.pub
+# Private GitLab server
+Host gitlab.company.com
+RSAAuthentication yes
+IdentityFile ~/.ssh/config/private-key-filename
```
+Due to the wide variety of SSH clients and their very large number of
+configuration options, further explanation of these topics is beyond the scope
+of this document.
+
+Public SSH keys need to be unique, as they will bind to your account.
+Your SSH key is the only identifier you'll have when pushing code via SSH.
+That's why it needs to uniquely map to a single user.
+
## Deploy keys
Deploy keys allow read-only access to multiple projects with a single SSH
@@ -89,10 +155,10 @@ If you want to add the same key to another project, please enable it in the
list that says 'Deploy keys from projects available to you'. All the deploy
keys of all the projects you have access to are available. This project
access can happen through being a direct member of the project, or through
-a group. See `def accessible_deploy_keys` in `app/models/user.rb` for more
-information.
+a group.
-Deploy keys can be shared between projects, you just need to add them to each project.
+Deploy keys can be shared between projects, you just need to add them to each
+project.
## Applications
@@ -100,33 +166,4 @@ Deploy keys can be shared between projects, you just need to add them to each pr
How to add your ssh key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration
-## Tip: Non-default OpenSSH key file names or locations
-
-If, for whatever reason, you decide to specify a non-default location and filename for your GitLab SSH key pair, you must configure your SSH client to find your GitLab SSH private key for connections to your GitLab server (perhaps gitlab.com). For OpenSSH clients, this is handled in the `~/.ssh/config` file with a stanza similar to the following:
-
-```
-#
-# Main gitlab.com server
-#
-Host gitlab.com
-RSAAuthentication yes
-IdentityFile ~/my-ssh-key-directory/my-gitlab-private-key-filename
-User mygitlabusername
-```
-
-Another example
-```
-#
-# Our company's internal GitLab server
-#
-Host my-gitlab.company.com
-RSAAuthentication yes
-IdentityFile ~/my-ssh-key-directory/company-com-private-key-filename
-```
-
-Note in the gitlab.com example above a username was specified to override the default chosen by OpenSSH (your local username). This is only required if your local and remote usernames differ.
-
-Due to the wide variety of SSH clients and their very large number of configuration options, further explanation of these topics is beyond the scope of this document.
-
-Public SSH keys need to be unique, as they will bind to your account. Your SSH key is the only identifier you'll
-have when pushing code via SSH. That's why it needs to uniquely map to a single user. \ No newline at end of file
+[winputty]: https://the.earth.li/~sgtatham/putty/0.67/htmldoc/Chapter8.html#pubkey-puttygen
diff --git a/doc/university/README.md b/doc/university/README.md
index 49714e4fb59..12727e9d56f 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -5,7 +5,7 @@ GitLab University is the best place to learn about **Version Control with Git an
It doesn't replace, but accompanies our great [Documentation](https://docs.gitlab.com)
and [Blog Articles](https://about.gitlab.com/blog/).
-Would you like to contribute to GitLab University? Then please take a look at our contribution [process](/process) for more information.
+Would you like to contribute to GitLab University? Then please take a look at our contribution [process](process) for more information.
## Gitlab University Curriculum
@@ -19,7 +19,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
---
-### 1. <a name="beginner"></a> GitLab Beginner
+### 1. GitLab Beginner
#### 1.1. Version Control and Git
@@ -85,7 +85,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
---
-### 2. <a name="intermediate"></a> GitLab Intermediate
+### 2. GitLab Intermediate
#### 2.1 GitLab Pages
@@ -97,7 +97,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [Markdown in GitLab](../user/markdown.md)
1. [Issues and Merge Requests - Video](https://www.youtube.com/watch?v=raXvuwet78M)
-1. [Due Dates and Milestones fro GitLab Issues](https://about.gitlab.com/2016/08/05/feature-highlight-set-dates-for-issues/)
+1. [Due Dates and Milestones for GitLab Issues](https://about.gitlab.com/2016/08/05/feature-highlight-set-dates-for-issues/)
1. [How to Use GitLab Labels](https://about.gitlab.com/2016/08/17/using-gitlab-labels/)
1. [Applying GitLab Labels Automatically](https://about.gitlab.com/2016/08/19/applying-gitlab-labels-automatically/)
1. [GitLab Issue Board - Product Page](https://about.gitlab.com/solutions/issueboard/)
@@ -141,13 +141,13 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
---
-### 3. <a name="advanced"></a> GitLab Advanced
+### 3. GitLab Advanced
#### 3.1. Dev Ops
1. [Xebia Labs: Dev Ops Terminology](https://xebialabs.com/glossary/)
1. [Xebia Labs: Periodic Table of DevOps Tools](https://xebialabs.com/periodic-table-of-devops-tools/)
-1. [Puppet Labs: State of Dev Ops 2015 - Book](https://puppetlabs.com/sites/default/files/2015-state-of-devops-report.pdf)
+1. [Puppet Labs: State of Dev Ops 2016 - Book](https://puppet.com/resources/white-paper/2016-state-of-devops-report)
#### 3.2. Installing GitLab with Omnibus
@@ -173,7 +173,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
#### 3.6 Custom Languages
-1. [How to add Syntax Highlighting Support for Custom Langauges to GitLab - Video](how to add support for your favorite language to GitLab)
+1. [How to add Syntax Highlighting Support for Custom Languages to GitLab - Video](https://youtu.be/6WxTMqatrrA)
#### 3.7. Scalability and High Availability
@@ -186,7 +186,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [GitLab Cycle Analytics Overview](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/)
1. [GitLab Cycle Analytics - Product Page](https://about.gitlab.com/solutions/cycle-analytics/)
-#### 3.9. <a name="integrations"></a> Integrations
+#### 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)
@@ -198,7 +198,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
---
-## 4. <a name="external"></a> External Articles
+### 4. External Articles
1. [2011 WSJ article by Marc Andreessen - Software is Eating the World](http://www.wsj.com/articles/SB10001424053111903480904576512250915629460)
1. [2014 Blog post by Chris Dixon - Software eats software development](http://cdixon.org/2014/04/13/software-eats-software-development/)
@@ -206,11 +206,14 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
---
-## 5. <a name="team"></a> Resources for GitLab Team Members
+### 5. Resources for GitLab Team Members
*Some content can only be accessed by GitLab team members*
1. [Support Path](support/README.md)
1. [Sales Path (redirect to sales handbook)](https://about.gitlab.com/handbook/sales-onboarding/)
+1. [User Training](training/user_training.md)
+1. [GitLab Flow Training](training/gitlab_flow.md)
+1. [Training Topics](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/university/training/topics/)
1. [GitLab architecture for noobs](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/development/architecture.md)
1. [Client Assessment of GitLab versus GitHub](https://docs.google.com/a/gitlab.com/spreadsheets/d/18cRF9Y5I6I7Z_ab6qhBEW55YpEMyU4PitZYjomVHM-M/edit?usp=sharing)
diff --git a/doc/university/high-availability/aws/img/auto-scaling-det.png b/doc/university/high-availability/aws/img/auto-scaling-det.png
index e9b65529495..1e125f301bc 100644
--- a/doc/university/high-availability/aws/img/auto-scaling-det.png
+++ b/doc/university/high-availability/aws/img/auto-scaling-det.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/db-subnet-group.png b/doc/university/high-availability/aws/img/db-subnet-group.png
index 0768aa73c45..590a02b8dbe 100644
--- a/doc/university/high-availability/aws/img/db-subnet-group.png
+++ b/doc/university/high-availability/aws/img/db-subnet-group.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/ec-subnet.png b/doc/university/high-availability/aws/img/ec-subnet.png
index f41d78b271d..43ef76b62d3 100644
--- a/doc/university/high-availability/aws/img/ec-subnet.png
+++ b/doc/university/high-availability/aws/img/ec-subnet.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/elastic-file-system.png b/doc/university/high-availability/aws/img/elastic-file-system.png
index 7de866d1e89..5bcfb8d0588 100644
--- a/doc/university/high-availability/aws/img/elastic-file-system.png
+++ b/doc/university/high-availability/aws/img/elastic-file-system.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/ig-rt.png b/doc/university/high-availability/aws/img/ig-rt.png
index 93bb0c2ae02..62cca074a1e 100644
--- a/doc/university/high-availability/aws/img/ig-rt.png
+++ b/doc/university/high-availability/aws/img/ig-rt.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/ig.png b/doc/university/high-availability/aws/img/ig.png
index cc50456370f..d4fc2d12de8 100644
--- a/doc/university/high-availability/aws/img/ig.png
+++ b/doc/university/high-availability/aws/img/ig.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/instance_specs.png b/doc/university/high-availability/aws/img/instance_specs.png
index ef31dc41dae..650f375ab3c 100644
--- a/doc/university/high-availability/aws/img/instance_specs.png
+++ b/doc/university/high-availability/aws/img/instance_specs.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/new_vpc.png b/doc/university/high-availability/aws/img/new_vpc.png
index 4aac6af7c7a..e51c066cee2 100644
--- a/doc/university/high-availability/aws/img/new_vpc.png
+++ b/doc/university/high-availability/aws/img/new_vpc.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/policies.png b/doc/university/high-availability/aws/img/policies.png
index 8c58117e4fa..afcd9e4af9b 100644
--- a/doc/university/high-availability/aws/img/policies.png
+++ b/doc/university/high-availability/aws/img/policies.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/rds-net-opt.png b/doc/university/high-availability/aws/img/rds-net-opt.png
index bc204de2474..651cc23b1ab 100644
--- a/doc/university/high-availability/aws/img/rds-net-opt.png
+++ b/doc/university/high-availability/aws/img/rds-net-opt.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/rds-sec-group.png b/doc/university/high-availability/aws/img/rds-sec-group.png
index 8864dc3e463..c6d1bc350e4 100644
--- a/doc/university/high-availability/aws/img/rds-sec-group.png
+++ b/doc/university/high-availability/aws/img/rds-sec-group.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/redis-cluster-det.png b/doc/university/high-availability/aws/img/redis-cluster-det.png
index 9e9a81283c5..51d3a08eab6 100644
--- a/doc/university/high-availability/aws/img/redis-cluster-det.png
+++ b/doc/university/high-availability/aws/img/redis-cluster-det.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/redis-net.png b/doc/university/high-availability/aws/img/redis-net.png
index 037bd6d6897..9022a9ada78 100644
--- a/doc/university/high-availability/aws/img/redis-net.png
+++ b/doc/university/high-availability/aws/img/redis-net.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/route_table.png b/doc/university/high-availability/aws/img/route_table.png
index 1dea322474d..c8bef75f01a 100644
--- a/doc/university/high-availability/aws/img/route_table.png
+++ b/doc/university/high-availability/aws/img/route_table.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/subnet.png b/doc/university/high-availability/aws/img/subnet.png
index dbc71201992..de910edc948 100644
--- a/doc/university/high-availability/aws/img/subnet.png
+++ b/doc/university/high-availability/aws/img/subnet.png
Binary files differ
diff --git a/doc/university/training/gitlab_flow.md b/doc/university/training/gitlab_flow.md
new file mode 100755
index 00000000000..a7db1f2e069
--- /dev/null
+++ b/doc/university/training/gitlab_flow.md
@@ -0,0 +1,53 @@
+# GitLab Flow
+
+- A simplified branching strategy
+- All features and fixes first go to master
+- Allows for 'production' or 'stable' branches
+- Bug fixes/hot fix patches are cherry-picked from master
+
+---
+
+# Feature branches
+
+- Create a feature/bugfix branch to do all work
+- Use merge requests to merge to master
+
+![inline](gitlab_flow/feature_branches.png)
+
+---
+
+# Production branch
+
+- One, long-running production release branch
+ as opposed to individual stable branches
+- Consider creating a tag for each version that gets deployed
+
+---
+
+# Production branch
+
+![inline](gitlab_flow/production_branch.png)
+
+---
+
+# Release branch
+
+- Useful if you release software to customers
+- When preparing a new release, create stable branch
+ from master
+- Consider creating a tag for each version
+- Cherry-pick critical bug fixes to stable branch for patch release
+- Never commit bug fixes directly to stable branch
+
+---
+
+# Release branch
+
+![inline](gitlab_flow/release_branches.png)
+
+---
+
+# More details
+
+Blog post on 'GitLab Flow' at
+[http://doc.gitlab.com/ee/workflow/gitlab_flow.html](http://doc.gitlab.com/ee/workflow/gitlab_flow.html)
diff --git a/doc/university/training/gitlab_flow/feature_branches.png b/doc/university/training/gitlab_flow/feature_branches.png
new file mode 100644
index 00000000000..612e0248222
--- /dev/null
+++ b/doc/university/training/gitlab_flow/feature_branches.png
Binary files differ
diff --git a/doc/university/training/gitlab_flow/production_branch.png b/doc/university/training/gitlab_flow/production_branch.png
new file mode 100644
index 00000000000..66456cc51af
--- /dev/null
+++ b/doc/university/training/gitlab_flow/production_branch.png
Binary files differ
diff --git a/doc/university/training/gitlab_flow/release_branches.png b/doc/university/training/gitlab_flow/release_branches.png
new file mode 100644
index 00000000000..5661e36c4e2
--- /dev/null
+++ b/doc/university/training/gitlab_flow/release_branches.png
Binary files differ
diff --git a/doc/university/training/index.md b/doc/university/training/index.md
new file mode 100755
index 00000000000..03179ff5a77
--- /dev/null
+++ b/doc/university/training/index.md
@@ -0,0 +1,6 @@
+# GitLab Training Material
+
+All GitLab training material is stored in markdown format. Slides are
+generated using [Deskset](http://www.decksetapp.com/).
+
+All training material is open to public contribution.
diff --git a/doc/university/training/logo.png b/doc/university/training/logo.png
new file mode 100644
index 00000000000..c80f65c053e
--- /dev/null
+++ b/doc/university/training/logo.png
Binary files differ
diff --git a/doc/university/training/topics/additional_resources.md b/doc/university/training/topics/additional_resources.md
new file mode 100755
index 00000000000..1ee615432aa
--- /dev/null
+++ b/doc/university/training/topics/additional_resources.md
@@ -0,0 +1,8 @@
+## Additional Resources
+
+1. GitLab Documentation [http://docs.gitlab.com](http://docs.gitlab.com/)
+2. GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/guis)
+3. Pro git book [http://git-scm.com/book](http://git-scm.com/book)
+4. Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/)
+5. Code School tutorial [http://try.github.io/](http://try.github.io/)
+6. Contact Us - [subscribers@gitlab.com](subscribers@gitlab.com)
diff --git a/doc/university/training/topics/agile_git.md b/doc/university/training/topics/agile_git.md
new file mode 100755
index 00000000000..e6e4fea9b51
--- /dev/null
+++ b/doc/university/training/topics/agile_git.md
@@ -0,0 +1,33 @@
+# Agile and Git
+
+----------
+
+## Agile
+
+Lean software development methods focused on collaboration and interaction
+with fast and smaller deployment cycles.
+
+----------
+
+## Where Git comes in
+
+Git is an excellent tool for an Agile team considering that it allows
+decentralized and simultaneous development.
+
+----------
+
+### Branching And Workflows
+
+Branching in an Agile environment usually happens around user stories with one
+or more developers working on it.
+
+If more than one developer then another branch for each developer is also used
+with his/her initials, and US id.
+
+After its tested merge into master and remove the branch.
+
+----------
+
+## What about GitLab
+Tools like GitLab enhance collaboration by adding dialog around code mainly
+through issues and merge requests.
diff --git a/doc/university/training/topics/bisect.md b/doc/university/training/topics/bisect.md
new file mode 100755
index 00000000000..a60c4365e0c
--- /dev/null
+++ b/doc/university/training/topics/bisect.md
@@ -0,0 +1,81 @@
+# Bisect
+
+----------
+
+## Bisect
+
+- Find a commit that introduced a bug
+- Works through a process of elimination
+- Specify a known good and bad revision to begin
+
+----------
+
+## Bisect
+
+1. Start the bisect process
+2. Enter the bad revision (usually latest commit)
+3. Enter a known good revision (commit/branch)
+4. Run code to see if bug still exists
+5. Tell bisect the result
+6. Repeat the previous 2 items until you find the offending commit
+
+----------
+
+## Setup
+
+```
+ mkdir bisect-ex
+ cd bisect-ex
+ touch index.html
+ git add -A
+ git commit -m "starting out"
+ vi index.html
+ # Add all good
+ git add -A
+ git commit -m "second commit"
+ vi index.html
+ # Add all good 2
+ git add -A
+ git commit -m "third commit"
+ vi index.html
+```
+
+----------
+
+```
+ # Add all good 3
+ git add -A
+ git commit -m "fourth commit"
+ vi index.html
+ # This looks bad
+ git add -A
+ git commit -m "fifth commit"
+ vi index.html
+ # Really bad
+ git add -A
+ git commit -m "sixth commit"
+ vi index.html
+ # again just bad
+ git add -A
+ git commit -m "seventh commit"
+```
+
+----------
+
+## Commands
+
+```
+ git bisect start
+ # Test your code
+ git bisect bad
+ git bisect next
+ # Say yes to the warning
+ # Test
+ git bisect good
+ # Test
+ git bisect bad
+ # Test
+ git bisect good
+ # done
+ git bisect reset
+```
diff --git a/doc/university/training/topics/cherry_picking.md b/doc/university/training/topics/cherry_picking.md
new file mode 100755
index 00000000000..af7a70a2818
--- /dev/null
+++ b/doc/university/training/topics/cherry_picking.md
@@ -0,0 +1,39 @@
+# Cherry Pick
+
+----------
+
+## Cherry Pick
+
+- Given an existing commit on one branch, apply the change to another branch
+- Useful for backporting bug fixes to previous release branches
+- Make the commit on the master branch and pick in to stable
+
+----------
+
+## Cherry Pick
+
+1. Check out a new 'stable' branch from 'master'
+1. Change back to 'master'
+1. Edit '`cherry_pick.rb`' and commit the changes.
+1. Check commit log to get the commit SHA
+1. Check out the 'stable' branch
+1. Cherry pick the commit using the SHA obtained earlier
+
+----------
+
+## Commands
+
+```bash
+git checkout master
+git checkout -b stable
+git checkout master
+
+# Edit `cherry_pick.rb`
+git add cherry_pick.rb
+git commit -m 'Fix bugs in cherry_pick.rb'
+git log
+# Copy commit SHA
+git checkout stable
+
+git cherry-pick <commit SHA>
+```
diff --git a/doc/university/training/topics/env_setup.md b/doc/university/training/topics/env_setup.md
new file mode 100755
index 00000000000..8149379b36f
--- /dev/null
+++ b/doc/university/training/topics/env_setup.md
@@ -0,0 +1,60 @@
+# Configure your environment
+
+----------
+## Install
+
+- **Windows**
+ - Install 'Git for Windows' from https://git-for-windows.github.io
+
+- **Mac**
+ - Type '`git`' in the Terminal application.
+ - If it's not installed, it will prompt you to install it.
+
+- **Linux**
+ ```bash
+ sudo yum install git-all
+ ```
+ ```bash
+ sudo apt-get install git-all
+ ```
+
+----------
+
+## Configure Git
+
+One-time configuration of the Git client
+
+```bash
+git config --global user.name "Your Name"
+git config --global user.email you@example.com
+```
+
+----------
+
+## Configure SSH Key
+
+```bash
+ssh-keygen -t rsa -b 4096 -C "you@computer-name"
+```
+
+```bash
+# You will be prompted for the following information. Press enter to accept the defaults. Defaults appear in parentheses.
+Generating public/private rsa key pair.
+Enter file in which to save the key (/Users/you/.ssh/id_rsa):
+Enter passphrase (empty for no passphrase):
+Enter same passphrase again:
+Your identification has been saved in /Users/you/.ssh/id_rsa.
+Your public key has been saved in /Users/you/.ssh/id_rsa.pub.
+The key fingerprint is:
+39:fc:ce:94:f4:09:13:95:64:9a:65:c1:de:05:4d:01 you@computer-name
+```
+
+Copy your public key and add it to your GitLab profile
+
+```bash
+cat ~/.ssh/id_rsa.pub
+```
+
+```bash
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQEL17Ufacg8cDhlQMS5NhV8z3GHZdhCrZbl4gz you@example.com
+```
diff --git a/doc/university/training/topics/explore_gitlab.md b/doc/university/training/topics/explore_gitlab.md
new file mode 100755
index 00000000000..b65457728c0
--- /dev/null
+++ b/doc/university/training/topics/explore_gitlab.md
@@ -0,0 +1,10 @@
+# Explore GitLab projects
+
+----------
+
+- Dashboard
+- User Preferences
+- Issues
+- Milestones and Labels
+- Manage project members
+- Project settings
diff --git a/doc/university/training/topics/feature_branching.md b/doc/university/training/topics/feature_branching.md
new file mode 100755
index 00000000000..4b34406ea75
--- /dev/null
+++ b/doc/university/training/topics/feature_branching.md
@@ -0,0 +1,32 @@
+# Feature branching
+
+----------
+
+- Efficient parallel workflow for teams
+- Develop each feature in a branch
+- Keeps changes isolated
+- Consider a 1-to-1 link to issues
+- Push branches to the server frequently
+ - Hint: This is a cheap backup for your work-in-progress code
+
+----------
+
+## Feature branching
+
+1. Create a new feature branch called 'squash_some_bugs'
+1. Edit '`bugs.rb`' and remove all the bugs.
+1. Commit
+1. Push
+
+----------
+
+## Commands
+
+```
+git checkout -b squash_some_bugs
+# Edit `bugs.rb`
+git status
+git add bugs.rb
+git commit -m 'Fix some buggy code'
+git push origin squash_some_bugs
+```
diff --git a/doc/university/training/topics/getting_started.md b/doc/university/training/topics/getting_started.md
new file mode 100755
index 00000000000..ec7bb2631aa
--- /dev/null
+++ b/doc/university/training/topics/getting_started.md
@@ -0,0 +1,95 @@
+# Getting Started
+
+----------
+
+## Instantiating Repositories
+
+* Create a new repository by instantiating it through
+```bash
+git init
+```
+* Copy an existing project by cloning the repository through
+```bash
+git clone <url>
+```
+
+----------
+
+## Central Repos
+
+* To instantiate a central repository a `--bare` flag is required.
+* Bare repositories don't allow file editing or committing changes.
+* Create a bare repo with
+```bash
+git init --bare project-name.git
+```
+
+----------
+
+## Instantiate workflow with clone
+
+1. Create a project in your user namespace
+ - Choose to import from 'Any Repo by URL' and use
+ https://gitlab.com/gitlab-org/training-examples.git
+2. Create a '`Workspace`' directory in your home directory.
+3. Clone the '`training-examples`' project
+
+----------
+
+## Commands
+
+```
+mkdir ~/workspace
+cd ~/workspace
+
+git clone git@gitlab.example.com:<username>/training-examples.git
+cd training-examples
+```
+----------
+
+## Git concepts
+
+**Untracked files**
+
+New files that Git has not been told to track previously.
+
+**Working area**
+
+Files that have been modified but are not committed.
+
+**Staging area**
+
+Modified files that have been marked to go in the next commit.
+
+----------
+
+## Committing Workflow
+
+1. Edit '`edit_this_file.rb`' in '`training-examples`'
+1. See it listed as a changed file (working area)
+1. View the differences
+1. Stage the file
+1. Commit
+1. Push the commit to the remote
+1. View the git log
+
+----------
+
+## Commands
+
+```
+# Edit `edit_this_file.rb`
+git status
+git diff
+git add <file>
+git commit -m 'My change'
+git push origin master
+git log
+```
+
+----------
+
+## Note
+
+* git fetch vs pull
+* Pull is git fetch + git merge
diff --git a/doc/university/training/topics/git_add.md b/doc/university/training/topics/git_add.md
new file mode 100755
index 00000000000..9ffb4b9c859
--- /dev/null
+++ b/doc/university/training/topics/git_add.md
@@ -0,0 +1,33 @@
+# Git Add
+
+----------
+
+## Git Add
+
+Adds content to the index or staging area.
+
+* Adds a list of file
+```bash
+git add <files>
+```
+* Adds all files including deleted ones
+```bash
+git add -A
+```
+
+----------
+
+## Git add continued
+
+* Add all text files in current dir
+```bash
+git add *.txt
+```
+* Add all text file in the project
+```bash
+git add "*.txt*"
+```
+* Adds all files in directory
+```bash
+git add views/layouts/
+```
diff --git a/doc/university/training/topics/git_intro.md b/doc/university/training/topics/git_intro.md
new file mode 100755
index 00000000000..ca1ff29d93b
--- /dev/null
+++ b/doc/university/training/topics/git_intro.md
@@ -0,0 +1,24 @@
+# Git introduction
+
+----------
+
+## Intro
+
+https://git-scm.com/about
+
+- Distributed version control
+ - Does not rely on connection to a central server
+ - Many copies of the complete history
+- Powerful branching and merging
+- Adapts to nearly any workflow
+- Fast, reliable and stable file format
+
+----------
+
+## Help!
+
+Use the tools at your disposal when you get stuck.
+
+- Use '`git help <command>`' command
+- Use Google
+- Read documentation at https://git-scm.com
diff --git a/doc/university/training/topics/git_log.md b/doc/university/training/topics/git_log.md
new file mode 100755
index 00000000000..32ebceff491
--- /dev/null
+++ b/doc/university/training/topics/git_log.md
@@ -0,0 +1,57 @@
+# Git Log
+
+----------
+
+Git log lists commit history. It allows searching and filtering.
+
+* Initiate log
+```
+git log
+```
+
+* Retrieve set number of records:
+```
+git log -n 2
+```
+
+* Search commits by author. Allows user name or a regular expression.
+```
+git log --author="user_name"
+```
+
+----------
+
+* Search by comment message.
+```
+git log --grep="<pattern>"
+```
+
+* Search by date
+```
+git log --since=1.month.ago --until=3.weeks.ago
+```
+
+
+----------
+
+## Git Log Workflow
+
+1. Change to workspace directory
+2. Clone the multi runner projects
+3. Change to project dir
+4. Search by author
+5. Search by date
+6. Combine
+
+----------
+
+## Commands
+
+```
+cd ~/workspace
+git clone git@gitlab.com:gitlab-org/gitlab-ci-multi-runner.git
+cd gitlab-ci-multi-runner
+git log --author="Travis"
+git log --since=1.month.ago --until=3.weeks.ago
+git log --since=1.month.ago --until=1.day.ago --author="Travis"
+```
diff --git a/doc/university/training/topics/gitlab_flow.md b/doc/university/training/topics/gitlab_flow.md
new file mode 100755
index 00000000000..8e5d3baf959
--- /dev/null
+++ b/doc/university/training/topics/gitlab_flow.md
@@ -0,0 +1,53 @@
+# GitLab Flow
+
+----------
+
+- A simplified branching strategy
+- All features and fixes first go to master
+- Allows for 'production' or 'stable' branches
+- Bug fixes/hot fix patches are cherry-picked from master
+
+----------
+
+### Feature branches
+
+- Create a feature/bugfix branch to do all work
+- Use merge requests to merge to master
+
+![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/feature_branches.png)
+
+----------
+
+## Production branch
+
+- One, long-running production release branch
+ as opposed to individual stable branches
+- Consider creating a tag for each version that gets deployed
+
+----------
+
+## Production branch
+
+![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/production_branch.png)
+
+----------
+
+## Release branch
+
+- Useful if you release software to customers
+- When preparing a new release, create stable branch
+ from master
+- Consider creating a tag for each version
+- Cherry-pick critical bug fixes to stable branch for patch release
+- Never commit bug fixes directly to stable branch
+
+----------
+
+![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/release_branches.png)
+
+----------
+
+## More details
+
+Blog post on 'GitLab Flow' at
+[http://doc.gitlab.com/ee/workflow/gitlab_flow.html](http://doc.gitlab.com/ee/workflow/gitlab_flow.html)
diff --git a/doc/university/training/topics/merge_conflicts.md b/doc/university/training/topics/merge_conflicts.md
new file mode 100755
index 00000000000..77807b3e7ef
--- /dev/null
+++ b/doc/university/training/topics/merge_conflicts.md
@@ -0,0 +1,70 @@
+# Merge conflicts
+
+----------
+
+- Happen often
+- Learning to fix conflicts is hard
+- Practice makes perfect
+- Force push after fixing conflicts. Be careful!
+
+----------
+
+## Merge conflicts
+
+1. Checkout a new branch and edit `conflicts.rb`. Add 'Line4' and 'Line5'.
+2. Commit and push
+3. Checkout master and edit `conflicts.rb`. Add 'Line6' and 'Line7' below 'Line3'.
+4. Commit and push to master
+5. Create a merge request and watch it fail
+6. Rebase our new branch with master
+7. Fix conflicts on the `conflicts.rb` file.
+8. Stage the file and continue rebasing
+9. Force push the changes
+10. Finally continue with the Merge Request
+
+----------
+
+## Commands
+
+```
+git checkout -b conflicts_branch
+
+# vi conflicts.rb
+# Add 'Line4' and 'Line5'
+
+git commit -am "add line4 and line5"
+git push origin conflicts_branch
+
+git checkout master
+
+# vi conflicts.rb
+# Add 'Line6' and 'Line7'
+git commit -am "add line6 and line7"
+git push origin master
+```
+
+Create a merge request on the GitLab web UI. You'll see a conflict warning.
+
+```
+git checkout conflicts_branch
+git fetch
+git rebase master
+
+# Fix conflicts by editing the files.
+
+git add conflicts.rb
+# No need to commit this file
+
+git rebase --continue
+
+# Remember that we have rewritten our commit history so we
+# need to force push so that our remote branch is restructured
+git push origin conflicts_branch -f
+```
+----------
+
+## Note
+* When to use 'git merge' and when to use 'git rebase'
+* Rebase when updating your branch with master
+* Merge when bringing changes from feature to master
+* Reference: https://www.atlassian.com/git/tutorials/merging-vs-rebasing/
diff --git a/doc/university/training/topics/merge_requests.md b/doc/university/training/topics/merge_requests.md
new file mode 100755
index 00000000000..5b446f02f63
--- /dev/null
+++ b/doc/university/training/topics/merge_requests.md
@@ -0,0 +1,43 @@
+# Merge requests
+
+----------
+
+- When you want feedback create a merge request
+- Target is the default branch (usually master)
+- Assign or mention the person you would like to review
+- Add 'WIP' to the title if it's a work in progress
+- When accepting, always delete the branch
+- Anyone can comment, not just the assignee
+- Push corrections to the same branch
+
+----------
+
+## Merge requests
+
+**Create your first merge request**
+
+1. Use the blue button in the activity feed
+1. View the diff (changes) and leave a comment
+1. Push a new commit to the same branch
+1. Review the changes again and notice the update
+
+----------
+
+## Feedback and Collaboration
+
+- Merge requests are a time for feedback and collaboration
+- Giving feedback is hard
+- Be as kind as possible
+- Receiving feedback is hard
+- Be as receptive as possible
+- Feedback is about the best code, not the person. You are not your code
+
+----------
+
+## Feedback and Collaboration
+
+Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests:
+[https://github.com/thoughtbot/guides/tree/master/code-review](https://github.com/thoughtbot/guides/tree/master/code-review)
+
+See GitLab merge requests for examples:
+[https://gitlab.com/gitlab-org/gitlab-ce/merge_requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests)
diff --git a/doc/university/training/topics/rollback_commits.md b/doc/university/training/topics/rollback_commits.md
new file mode 100755
index 00000000000..cf647284604
--- /dev/null
+++ b/doc/university/training/topics/rollback_commits.md
@@ -0,0 +1,81 @@
+# Rollback Commits
+
+----------
+
+## Undo Commits
+
+* Undo last commit putting everything back into the staging area.
+```
+git reset --soft HEAD^
+```
+
+* Add files and change message with:
+```
+git commit --amend -m "New Message"
+```
+
+----------
+
+* Undo last and remove changes
+```
+git reset --hard HEAD^
+```
+
+* Same as last one but for two commits back
+```
+git reset --hard HEAD^^
+```
+
+** Don't reset after pushing **
+
+----------
+
+## Reset Workflow
+
+1. Edit file again 'edit_this_file.rb'
+2. Check status
+3. Add and commit with wrong message
+4. Check log
+5. Amend commit
+6. Check log
+7. Soft reset
+8. Check log
+9. Pull for updates
+10. Push changes
+
+
+----------
+
+## Commands
+
+```
+# Change file edit_this_file.rb
+git status
+git commit -am "kjkfjkg"
+git log
+git commit --amend -m "New comment added"
+git log
+git reset --soft HEAD^
+git log
+git pull origin master
+git push origin master
+```
+
+----------
+
+## Note
+
+* git revert vs git reset
+* Reset removes the commit while revert removes the changes but leaves the commit
+* Revert is safer considering we can revert a revert
+
+```
+# Changed file
+git commit -am "bug introduced"
+git revert HEAD
+# New commit created reverting changes
+# Now we want to re apply the reverted commit
+git log # take hash from the revert commit
+git revert <rev commit hash>
+# reverted commit is back (new commit created again)
+```
diff --git a/doc/university/training/topics/stash.md b/doc/university/training/topics/stash.md
new file mode 100755
index 00000000000..c1bdda32645
--- /dev/null
+++ b/doc/university/training/topics/stash.md
@@ -0,0 +1,86 @@
+# Git Stash
+
+----------
+
+We use git stash to store our changes when they are not ready to be committed
+and we need to change to a different branch.
+
+* Stash
+```
+git stash save
+# or
+git stash
+# or with a message
+git stash save "this is a message to display on the list"
+```
+
+* Apply stash to keep working on it
+```
+git stash apply
+# or apply a specific one from out stack
+git stash apply stash@{3}
+```
+
+----------
+
+* Every time we save a stash it gets stacked so by using list we can see all our
+stashes.
+
+```
+git stash list
+# or for more information (log methods)
+git stash list --stat
+```
+
+* To clean our stack we need to manually remove them.
+
+```
+# drop top stash
+git stash drop
+# or
+git stash drop <name>
+# to clear all history we can use
+git stash clear
+```
+
+----------
+
+* Apply and drop on one command
+
+```
+ git stash pop
+```
+
+* If we meet conflicts we need to either reset or commit our changes.
+
+* Conflicts through `pop` will not drop a stash afterwards.
+
+----------
+
+## Git Stash
+
+1. Modify a file
+2. Stage file
+3. Stash it
+4. View our stash list
+5. Confirm no pending changes through status
+5. Apply with pop
+6. View list to confirm changes
+
+----------
+
+## Commands
+
+```
+# Modify edit_this_file.rb file
+git add .
+
+git stash save "Saving changes from edit this file"
+
+git stash list
+git status
+
+git stash pop
+git stash list
+git status
+```
diff --git a/doc/university/training/topics/subtree.md b/doc/university/training/topics/subtree.md
new file mode 100755
index 00000000000..5d869af64c1
--- /dev/null
+++ b/doc/university/training/topics/subtree.md
@@ -0,0 +1,55 @@
+## Subtree
+
+----------
+
+## Subtree
+
+* Used when there are nested repositories.
+* Not recommended when the amount of dependencies is too large
+* For these cases we need a dependency control system
+* Command are painfully long so aliases are necessary
+
+----------
+
+## Subtree Aliases
+
+* Add: git subtree add --prefix <target-folder> <url> <branch> --squash
+* Pull: git subtree add --prefix <target-folder> <url> <branch> --squash
+* Push: git subtree add --prefix <target-folder> <url> <branch>
+* Ex: git config alias.sbp 'subtree pull --prefix st /
+ git@gitlab.com:balameb/subtree-nested-example.git master --squash'
+
+----------
+
+```
+ # Add an alias
+ # Add
+ git config alias.sba 'subtree add --prefix st /
+ git@gitlab.com:balameb/subtree-nested-example.git master --squash'
+ # Pull
+ git config alias.sbpl 'subtree pull --prefix st /
+ git@gitlab.com:balameb/subtree-nested-example.git master --squash'
+ # Push
+ git config alias.sbph 'subtree push --prefix st /
+ git@gitlab.com:balameb/subtree-nested-example.git master'
+
+ # Adding this subtree adds a st dir with a readme
+ git sba
+ vi st/README.md
+ # Edit file
+ git status shows differences
+
+```
+
+----------
+
+```
+ # Adding, or committing won't change the sub repo at remote
+ # even if we push
+ git add -A
+ git commit -m "Adding to subtree readme"
+
+ # Push to subtree repo
+ git sbph
+ # now we can check our remote sub repo
+```
diff --git a/doc/university/training/topics/tags.md b/doc/university/training/topics/tags.md
new file mode 100755
index 00000000000..e9607b5a875
--- /dev/null
+++ b/doc/university/training/topics/tags.md
@@ -0,0 +1,38 @@
+# Tags
+
+----------
+
+- Useful for marking deployments and releases
+- Annotated tags are an unchangeable part of Git history
+- Soft/lightweight tags can be set and removed at will
+- Many projects combine an anotated release tag with a stable branch
+- Consider setting deployment/release tags automatically
+
+----------
+
+# Tags
+
+- Create a lightweight tag
+- Create an annotated tag
+- Push the tags to the remote repository
+
+**Additional resources**
+
+[http://git-scm.com/book/en/Git-Basics-Tagging](http://git-scm.com/book/en/Git-Basics-Tagging)
+
+----------
+
+# Commands
+
+```
+git checkout master
+
+# Lightweight tag
+git tag my_lightweight_tag
+
+# Annotated tag
+git tag -a v1.0 -m ‘Version 1.0’
+git tag
+
+git push origin --tags
+```
diff --git a/doc/university/training/topics/unstage.md b/doc/university/training/topics/unstage.md
new file mode 100755
index 00000000000..17dbb64b9e6
--- /dev/null
+++ b/doc/university/training/topics/unstage.md
@@ -0,0 +1,31 @@
+# Unstage
+
+----------
+
+## Unstage
+
+* To remove files from stage use reset HEAD. Where HEAD is the last commit of the current branch.
+
+```bash
+git reset HEAD <file>
+```
+
+* This will unstage the file but maintain the modifications. To revert the file back to the state it was in before the changes we can use:
+
+```bash
+git checkout -- <file>
+```
+
+----------
+
+* To remove a file from disk and repo use 'git rm' and to rm a dir use the '-r' flag.
+```
+git rm '*.txt'
+git rm -r <dirname>
+```
+
+
+* If we want to remove a file from the repository but keep it on disk, say we forgot to add it to our `.gitignore` file then use `--cache`.
+```
+git rm <filename> --cache
+```
diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md
new file mode 100755
index 00000000000..35afe73708f
--- /dev/null
+++ b/doc/university/training/user_training.md
@@ -0,0 +1,392 @@
+# GitLab Git Workshop
+
+---
+
+# Agenda
+
+1. Brief history of Git
+1. GitLab walkthrough
+1. Configure your environment
+1. Workshop
+
+---
+
+# Git introduction
+
+https://git-scm.com/about
+
+- Distributed version control
+ - Does not rely on connection to a central server
+ - Many copies of the complete history
+- Powerful branching and merging
+- Adapts to nearly any workflow
+- Fast, reliable and stable file format
+
+---
+
+# Help!
+
+Use the tools at your disposal when you get stuck.
+
+- Use '`git help <command>`' command
+- Use Google
+- Read documentation at https://git-scm.com
+
+---
+
+# GitLab Walkthrough
+
+![fit](logo.png)
+
+---
+
+# Configure your environment
+
+- Windows: Install 'Git for Windows'
+
+> https://git-for-windows.github.io
+
+- Mac: Type '`git`' in the Terminal application.
+
+> If it's not installed, it will prompt you to install it.
+
+- Debian: '`sudo apt-get install git-all`'
+or Red Hat '`sudo yum install git-all`'
+
+---
+
+# Git Workshop
+
+## Overview
+
+1. Configure Git
+1. Configure SSH Key
+1. Create a project
+1. Committing
+1. Feature branching
+1. Merge requests
+1. Feedback and Collaboration
+
+---
+
+# Configure Git
+
+One-time configuration of the Git client
+
+```bash
+git config --global user.name "Your Name"
+git config --global user.email you@example.com
+```
+
+---
+
+# Configure SSH Key
+
+```bash
+ssh-keygen -t rsa -b 4096 -C "you@computer-name"
+```
+
+```bash
+# You will be prompted for the following information. Press enter to accept the defaults. Defaults appear in parentheses.
+Generating public/private rsa key pair.
+Enter file in which to save the key (/Users/you/.ssh/id_rsa):
+Enter passphrase (empty for no passphrase):
+Enter same passphrase again:
+Your identification has been saved in /Users/you/.ssh/id_rsa.
+Your public key has been saved in /Users/you/.ssh/id_rsa.pub.
+The key fingerprint is:
+39:fc:ce:94:f4:09:13:95:64:9a:65:c1:de:05:4d:01 you@computer-name
+```
+
+Copy your public key and add it to your GitLab profile
+
+```bash
+cat ~/.ssh/id_rsa.pub
+```
+
+```bash
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQEL17Ufacg8cDhlQMS5NhV8z3GHZdhCrZbl4gz you@example.com
+```
+
+---
+
+# Create a project
+
+- Create a project in your user namespace
+ - Choose to import from 'Any Repo by URL' and use
+ https://gitlab.com/gitlab-org/training-examples.git
+- Create a '`development`' or '`workspace`' directory in your home directory.
+- Clone the '`training-examples`' project
+
+---
+
+# Commands
+
+```
+mkdir ~/development
+cd ~/development
+
+-or-
+
+mkdir ~/workspace
+cd ~/workspace
+
+git clone git@gitlab.example.com:<username>/training-examples.git
+cd training-examples
+```
+
+---
+
+# Git concepts
+
+**Untracked files**
+
+New files that Git has not been told to track previously.
+
+**Working area**
+
+Files that have been modified but are not committed.
+
+**Staging area**
+
+Modified files that have been marked to go in the next commit.
+
+---
+
+# Committing
+
+1. Edit '`edit_this_file.rb`' in '`training-examples`'
+1. See it listed as a changed file (working area)
+1. View the differences
+1. Stage the file
+1. Commit
+1. Push the commit to the remote
+1. View the git log
+
+---
+
+# Commands
+
+```
+# Edit `edit_this_file.rb`
+git status
+git diff
+git add <file>
+git commit -m 'My change'
+git push origin master
+git log
+```
+
+---
+
+# Feature branching
+
+- Efficient parallel workflow for teams
+- Develop each feature in a branch
+- Keeps changes isolated
+- Consider a 1-to-1 link to issues
+- Push branches to the server frequently
+ - Hint: This is a cheap backup for your work-in-progress code
+
+---
+
+# Feature branching
+
+1. Create a new feature branch called 'squash_some_bugs'
+1. Edit '`bugs.rb`' and remove all the bugs.
+1. Commit
+1. Push
+
+---
+
+# Commands
+
+```
+git checkout -b squash_some_bugs
+# Edit `bugs.rb`
+git status
+git add bugs.rb
+git commit -m 'Fix some buggy code'
+git push origin squash_some_bugs
+```
+
+---
+
+# Merge requests
+
+- When you want feedback create a merge request
+- Target is the ‘default’ branch (usually master)
+- Assign or mention the person you would like to review
+- Add 'WIP' to the title if it's a work in progress
+- When accepting, always delete the branch
+- Anyone can comment, not just the assignee
+- Push corrections to the same branch
+
+---
+
+# Merge requests
+
+**Create your first merge request**
+
+1. Use the blue button in the activity feed
+1. View the diff (changes) and leave a comment
+1. Push a new commit to the same branch
+1. Review the changes again and notice the update
+
+---
+
+# Feedback and Collaboration
+
+- Merge requests are a time for feedback and collaboration
+- Giving feedback is hard
+- Be as kind as possible
+- Receiving feedback is hard
+- Be as receptive as possible
+- Feedback is about the best code, not the person. You are not your code
+
+---
+
+# Feedback and Collaboration
+
+Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests:
+[https://github.com/thoughtbot/guides/tree/master/code-review](https://github.com/thoughtbot/guides/tree/master/code-review)
+
+See GitLab merge requests for examples:
+[https://gitlab.com/gitlab-org/gitlab-ce/merge_requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests)
+
+---
+
+# Explore GitLab projects
+
+![fit](logo.png)
+
+- Dashboard
+- User Preferences
+- ReadMe, Changelog, License shortcuts
+- Issues
+- Milestones and Labels
+- Manage project members
+- Project settings
+
+---
+
+# Tags
+
+- Useful for marking deployments and releases
+- Annotated tags are an unchangeable part of Git history
+- Soft/lightweight tags can be set and removed at will
+- Many projects combine an anotated release tag with a stable branch
+- Consider setting deployment/release tags automatically
+
+---
+
+# Tags
+
+- Create a lightweight tag
+- Create an annotated tag
+- Push the tags to the remote repository
+
+**Additional resources**
+
+[http://git-scm.com/book/en/Git-Basics-Tagging](http://git-scm.com/book/en/Git-Basics-Tagging)
+
+---
+
+# Commands
+
+```
+git checkout master
+
+# Lightweight tag
+git tag my_lightweight_tag
+
+# Annotated tag
+git tag -a v1.0 -m ‘Version 1.0’
+git tag
+
+git push origin --tags
+```
+
+---
+
+# Merge conflicts
+
+- Happen often
+- Learning to fix conflicts is hard
+- Practice makes perfect
+- Force push after fixing conflicts. Be careful!
+
+---
+
+# Merge conflicts
+
+1. Checkout a new branch and edit `conflicts.rb`. Add 'Line4' and 'Line5'.
+1. Commit and push
+1. Checkout master and edit `conflicts.rb`. Add 'Line6' and 'Line7' below 'Line3'.
+1. Commit and push to master
+1. Create a merge request
+
+---
+
+# Merge conflicts
+
+After creating a merge request you should notice that conflicts exist. Resolve
+the conflicts locally by rebasing.
+
+```
+git rebase master
+
+# Fix conflicts by editing the files.
+
+git add conflicts.rb
+git commit -m 'Fix conflicts'
+git rebase --continue
+git push origin <branch> -f
+```
+
+---
+
+# Rebase with squash
+
+You may end up with a commit log that looks like this:
+
+```
+Fix issue #13
+Test
+Fix
+Fix again
+Test
+Test again
+Does this work?
+```
+
+Squash these in to meaningful commits using an interactive rebase.
+
+---
+
+# Rebase with squash
+
+Squash the commits on the same branch we used for the merge conflicts step.
+
+```
+git rebase -i master
+```
+
+In the editor, leave the first commit as 'pick' and set others to 'fixup'.
+
+---
+
+# Questions?
+
+![fit](logo.png)
+
+Thank you for your hard work!
+
+**Additional Resources**
+
+GitLab Documentation [http://docs.gitlab.com](http://docs.gitlab.com/)
+GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/guis)
+Pro git book [http://git-scm.com/book](http://git-scm.com/book)
+Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/)
+Code School tutorial [http://try.github.io/](http://try.github.io/)
+Contact Us - [subscribers@gitlab.com](subscribers@gitlab.com)
diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md
index c0084d9d59c..8c0d3f78b55 100644
--- a/doc/update/8.12-to-8.13.md
+++ b/doc/update/8.12-to-8.13.md
@@ -166,7 +166,7 @@ See [smtp_settings.rb.sample] as an example.
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
-
+
For Ubuntu 16.04.1 LTS:
sudo systemctl daemon-reload
diff --git a/doc/update/8.13-to-8.14.md b/doc/update/8.13-to-8.14.md
index 46ea19d11d0..c64d3407461 100644
--- a/doc/update/8.13-to-8.14.md
+++ b/doc/update/8.13-to-8.14.md
@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-14-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v4.0.0
+sudo -u git -H git checkout v4.0.3
```
### 6. Update gitlab-workhorse
@@ -84,7 +84,7 @@ GitLab 8.1.
```bash
cd /home/git/gitlab-workhorse
sudo -u git -H git fetch --all
-sudo -u git -H git checkout v1.0.0
+sudo -u git -H git checkout v1.0.1
sudo -u git -H make
```
diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md
new file mode 100644
index 00000000000..4eacab0c890
--- /dev/null
+++ b/doc/update/8.14-to-8.15.md
@@ -0,0 +1,202 @@
+# From 8.14 to 8.15
+
+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
+
+ 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.1.tar.gz
+echo 'c39b4001f7acb4e334cb60a0f4df72d434bef711 ruby-2.3.1.tar.gz' | shasum --check - && tar xzf ruby-2.3.1.tar.gz
+cd ruby-2.3.1
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Get latest code
+
+```bash
+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
+sudo -u git -H git checkout 8-15-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-15-stable-ee
+```
+
+### 5. 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.0
+```
+
+### 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
+sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
+```
+
+### 7. 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
+```
+
+### 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
+git diff origin/8-14-stable:config/gitlab.yml.example origin/8-15-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
+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
+# For HTTPS configurations
+git diff origin/8-14-stable:lib/support/nginx/gitlab-ssl origin/8-15-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-14-stable:lib/support/nginx/gitlab origin/8-15-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-15-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-15-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+Ensure you're still up-to-date with the latest init script changes:
+
+ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
+
+### 9. Start application
+
+ sudo service gitlab start
+ sudo service nginx restart
+
+### 10. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+ 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:
+
+ 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.14)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.13 to 8.14](8.13-to-8.14.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/update/patch_versions.md b/doc/update/patch_versions.md
index 60729316cde..685972cfb41 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -1,7 +1,10 @@
# Universal update guide for patch versions
-*Make sure you view this [upgrade guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/patch_versions.md) from the `master` branch for the most up to date instructions.*
-For example from 7.14.0 to 7.14.3, also see the [semantic versioning specification](http://semver.org/).
+## Select Version to Install
+
+Make sure you view [this update guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/patch_versions.md) 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 in the top left corner of GitLab (below the menu bar).
### 0. Backup
@@ -45,10 +48,9 @@ sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`ca
### 4. Update gitlab-workhorse to the corresponding version
```bash
-cd /home/git/gitlab-workhorse
-sudo -u git -H git fetch
-sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` -b v`cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION`
-sudo -u git -H make
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
```
### 5. Install libs, migrations, etc.
diff --git a/doc/user/admin_area/img/admin_labels.png b/doc/user/admin_area/img/admin_labels.png
index 1ee33a534ab..a9ea059ccf9 100644
--- a/doc/user/admin_area/img/admin_labels.png
+++ b/doc/user/admin_area/img/admin_labels.png
Binary files differ
diff --git a/doc/user/admin_area/monitoring/img/health_check_token.png b/doc/user/admin_area/monitoring/img/health_check_token.png
index 2d7c82a65a8..182549fc484 100644
--- a/doc/user/admin_area/monitoring/img/health_check_token.png
+++ b/doc/user/admin_area/monitoring/img/health_check_token.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/access_restrictions.png b/doc/user/admin_area/settings/img/access_restrictions.png
index 8eea84320d7..8c5336c7835 100644
--- a/doc/user/admin_area/settings/img/access_restrictions.png
+++ b/doc/user/admin_area/settings/img/access_restrictions.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png b/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png
index 53f7e76033e..b7d6671902a 100644
--- a/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png
+++ b/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/admin_area_settings_button.png b/doc/user/admin_area/settings/img/admin_area_settings_button.png
index 509708b627f..1d2c0ac04bc 100644
--- a/doc/user/admin_area/settings/img/admin_area_settings_button.png
+++ b/doc/user/admin_area/settings/img/admin_area_settings_button.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/domain_blacklist.png b/doc/user/admin_area/settings/img/domain_blacklist.png
index bd87b73cf9e..dedd3be1e8f 100644
--- a/doc/user/admin_area/settings/img/domain_blacklist.png
+++ b/doc/user/admin_area/settings/img/domain_blacklist.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/restricted_url.png b/doc/user/admin_area/settings/img/restricted_url.png
index 8b00a18320b..67abd13f741 100644
--- a/doc/user/admin_area/settings/img/restricted_url.png
+++ b/doc/user/admin_area/settings/img/restricted_url.png
Binary files differ
diff --git a/doc/user/img/markdown_logo.png b/doc/user/img/markdown_logo.png
index 05c8b0d0ccf..bb3faaaec76 100644
--- a/doc/user/img/markdown_logo.png
+++ b/doc/user/img/markdown_logo.png
Binary files differ
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 7a7a0b864bd..f6484688721 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -1,43 +1,5 @@
# Markdown
-## Table of Contents
-
-**[GitLab Flavored Markdown](#gitlab-flavored-markdown-gfm)**
-
-* [Newlines](#newlines)
-* [Multiple underscores in words](#multiple-underscores-in-words)
-* [URL auto-linking](#url-auto-linking)
-* [Multiline Blockquote](#multiline-blockquote)
-* [Code and Syntax Highlighting](#code-and-syntax-highlighting)
-* [Inline Diff](#inline-diff)
-* [Emoji](#emoji)
-* [Special GitLab references](#special-gitlab-references)
-* [Task Lists](#task-lists)
-* [Videos](#videos)
-
-**[Standard Markdown](#standard-markdown)**
-
-* [Headers](#headers)
-* [Emphasis](#emphasis)
-* [Lists](#lists)
-* [Links](#links)
-* [Images](#images)
-* [Blockquotes](#blockquotes)
-* [Inline HTML](#inline-html)
-* [Horizontal Rule](#horizontal-rule)
-* [Line Breaks](#line-breaks)
-* [Tables](#tables)
-* [Footnotes](#footnotes)
-
-**[Wiki-Specific Markdown](#wiki-specific-markdown)**
-
-* [Wiki - Direct page link](#wiki-direct-page-link)
-* [Wiki - Direct file link](#wiki-direct-file-link)
-* [Wiki - Hierarchical link](#wiki-hierarchical-link)
-* [Wiki - Root link](#wiki-root-link)
-
-**[References](#references)**
-
## GitLab Flavored Markdown (GFM)
> **Note:**
@@ -64,7 +26,7 @@ You can use GFM in the following areas:
You can also use other rich text files in GitLab. You might have to install a
dependency to do so. Please see the [github-markup gem readme](https://github.com/gitlabhq/markup#markups) for more information.
-## Newlines
+### Newlines
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#newlines
@@ -84,7 +46,7 @@ Violets are blue
Sugar is sweet
-## Multiple underscores in words
+### Multiple underscores in words
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multiple-underscores-in-words
@@ -99,7 +61,7 @@ perform_complicated_task
do_this_and_do_that_and_another_thing
-## URL auto-linking
+### URL auto-linking
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#url-auto-linking
@@ -120,7 +82,7 @@ GFM will autolink almost any URL you copy and paste into your text:
* irc://irc.freenode.net/gitlab
* http://localhost:3000
-## Multiline Blockquote
+### Multiline Blockquote
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multiline-blockquote
@@ -154,7 +116,7 @@ multiple lines,
you can quote that without having to manually prepend `>` to every line!
>>>
-## Code and Syntax Highlighting
+### Code and Syntax Highlighting
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#code-and-syntax-highlighting
@@ -224,7 +186,7 @@ s = "There is no highlighting for this."
But let's throw in a <b>tag</b>.
```
-## Inline Diff
+### Inline Diff
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#inline-diff
@@ -240,7 +202,7 @@ However the wrapping tags cannot be mixed as such:
- {- deletions -]
- [- deletions -}
-## Emoji
+### Emoji
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji
@@ -265,7 +227,7 @@ If you are new to this, don't be :fearful:. You can easily join the emoji :famil
Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported emoji codes. :thumbsup:
-## Special GitLab References
+### Special GitLab References
GFM recognizes special references.
@@ -305,7 +267,19 @@ GFM also recognizes certain cross-project references:
| `namespace/project@9ba12248...b19a04f5` | commit range comparison |
| `namespace/project~"Some label"` | issues with given label |
-## Task Lists
+It also has a shorthand version to reference other projects from the same namespace:
+
+| input | references |
+|:------------------------------|:------------------------|
+| `project#123` | issue |
+| `project!123` | merge request |
+| `project%123` | milestone |
+| `project$123` | snippet |
+| `project@9ba12248` | specific commit |
+| `project@9ba12248...b19a04f5` | commit range comparison |
+| `project~"Some label"` | issues with given label |
+
+### Task Lists
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#task-lists
@@ -328,7 +302,7 @@ You can add task lists to issues, merge requests and comments. To create a task
Task lists can only be created in descriptions, not in titles. Task item state can be managed by editing the description's Markdown or by toggling the rendered check boxes.
-## Videos
+### Videos
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#videos
@@ -345,27 +319,45 @@ Here's a sample video:
![Sample Video](img/markdown_video.mp4)
-# Standard Markdown
+### Math
-## Headers
+> If this is not rendered correctly, see
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#math
-```no-highlight
-# H1
-## H2
-### H3
-#### H4
-##### H5
-###### H6
+It is possible to have math written with the LaTeX syntax rendered using [KaTeX][katex].
-Alternatively, for H1 and H2, an underline-ish style:
+Math written inside ```$``$``` will be rendered inline with the text.
-Alt-H1
-======
+Math written inside triple back quotes, with the language declared as `math`, will be rendered on a separate line.
-Alt-H2
-------
+Example:
+
+ This math is inline $`a^2+b^2=c^2`$.
+
+ This is on a separate line
+ ```math
+ a^2+b^2=c^2
+ ```
+
+Becomes:
+
+This math is inline $`a^2+b^2=c^2`$.
+
+This is on a separate line
+```math
+a^2+b^2=c^2
```
+_Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._
+
+>**Note:**
+This also works for the asciidoctor `:stem: latexmath`. For details see the [asciidoctor user manual][asciidoctor-manual].
+
+## Standard Markdown
+
+### Headers
+
+```no-highlight
# H1
## H2
### H3
@@ -380,6 +372,7 @@ Alt-H1
Alt-H2
------
+```
### Header IDs and links
@@ -416,7 +409,7 @@ Would generate the following link IDs:
Note that the Emoji processing happens before the header IDs are generated, so the Emoji is converted to an image which then gets removed from the ID.
-## Emphasis
+### Emphasis
```no-highlight
Emphasis, aka italics, with *asterisks* or _underscores_.
@@ -436,7 +429,7 @@ Combined emphasis with **asterisks and _underscores_**.
Strikethrough uses two tildes. ~~Scratch this.~~
-## Lists
+### Lists
```no-highlight
1. First ordered list item
@@ -492,7 +485,7 @@ the second list item will be incorrectly labeled as `1`.
Second paragraph of first item.
2. Another item
-## Links
+### Links
There are two ways to create links, inline-style and reference-style.
@@ -501,9 +494,9 @@ There are two ways to create links, inline-style and reference-style.
[I'm a reference-style link][Arbitrary case-insensitive reference text]
[I'm a relative reference to a repository file](LICENSE)
-
+
[I am an absolute reference within the repository](/doc/user/markdown.md)
-
+
[I link to the Milestones page](/../milestones)
[You can use numbers for reference-style link definitions][1]
@@ -523,9 +516,9 @@ There are two ways to create links, inline-style and reference-style.
[I'm a relative reference to a repository file](LICENSE)[^1]
[I am an absolute reference within the repository](/doc/user/markdown.md)
-
+
[I link to the Milestones page](/../milestones)
-
+
[You can use numbers for reference-style link definitions][1]
Or leave it empty and use the [link text itself][]
@@ -544,7 +537,8 @@ Relative links do not allow referencing project files in a wiki page or wiki pag
will point the link to `wikis/style` when the link is inside of a wiki markdown file.
-## Images
+
+### Images
Here's our logo (hover to see the title text):
@@ -568,7 +562,7 @@ Reference-style:
[logo]: img/markdown_logo.png
-## Blockquotes
+### Blockquotes
```no-highlight
> Blockquotes are very handy in email to emulate reply text.
@@ -586,11 +580,11 @@ Quote break.
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
-## Inline HTML
+### Inline HTML
You can also use raw HTML in your Markdown, and it'll mostly work pretty well.
-See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows `span` elements.
+See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/1.11.0/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows `span` elements.
```no-highlight
<dl>
@@ -610,7 +604,7 @@ See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubyd
<dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
</dl>
-## Horizontal Rule
+### Horizontal Rule
```
Three or more...
@@ -642,7 +636,7 @@ ___
Underscores
-## Line Breaks
+### Line Breaks
My basic recommendation for learning how line breaks work is to experiment and discover -- hit &lt;Enter&gt; once (i.e., insert one newline), then hit it twice (i.e., insert two newlines), see what happens. You'll soon learn to get what you want. "Markdown Toggle" is your friend.
@@ -656,7 +650,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa
This line is also a separate paragraph, but...
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
-This line is also a separate paragraph, and...
+This line is also a separate paragraph, and...
This line is on its own line, because the previous line ends with two
spaces.
```
@@ -668,11 +662,11 @@ This line is separated from the one above by two newlines, so it will be a *sepa
This line is also begins a separate paragraph, but...
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
-This line is also a separate paragraph, and...
+This line is also a separate paragraph, and...
This line is on its own line, because the previous line ends with two
spaces.
-## Tables
+### Tables
Tables aren't part of the core Markdown spec, but they are part of GFM and Markdown Here supports them.
@@ -708,16 +702,15 @@ By including colons in the header row, you can align the text within that column
| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 |
| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 |
-## Footnotes
-
-You can add footnotes to your text as follows.[^1]
-[^1]: This is my awesome footnote.
+### Footnotes
```
-You can add footnotes to your text as follows.[^1]
-[^1]: This is my awesome footnote.
+You can add footnotes to your text as follows.[^2]
+[^2]: This is my awesome footnote.
```
+You can add footnotes to your text as follows.[^2]
+
## Wiki-specific Markdown
The following examples show how links inside wikis behave.
@@ -752,30 +745,30 @@ A link can be constructed relative to the current wiki page using `./<page>`,
- If this snippet was placed on a page at `<your_wiki>/documentation/main`,
it would link to `<your_wiki>/documentation/related`:
- ```markdown
- [Link to Related Page](./related)
- ```
+ ```markdown
+ [Link to Related Page](./related)
+ ```
- If this snippet was placed on a page at `<your_wiki>/documentation/related/content`,
it would link to `<your_wiki>/documentation/main`:
- ```markdown
- [Link to Related Page](../main)
- ```
+ ```markdown
+ [Link to Related Page](../main)
+ ```
- If this snippet was placed on a page at `<your_wiki>/documentation/main`,
it would link to `<your_wiki>/documentation/related.md`:
- ```markdown
- [Link to Related Page](./related.md)
- ```
+ ```markdown
+ [Link to Related Page](./related.md)
+ ```
- If this snippet was placed on a page at `<your_wiki>/documentation/related/content`,
it would link to `<your_wiki>/documentation/main.md`:
- ```markdown
- [Link to Related Page](../main.md)
- ```
+ ```markdown
+ [Link to Related Page](../main.md)
+ ```
### Wiki - Root link
@@ -783,22 +776,28 @@ A link starting with a `/` is relative to the wiki root.
- This snippet links to `<wiki_root>/documentation`:
- ```markdown
- [Link to Related Page](/documentation)
- ```
+ ```markdown
+ [Link to Related Page](/documentation)
+ ```
- This snippet links to `<wiki_root>/miscellaneous.md`:
- ```markdown
- [Link to Related Page](/miscellaneous.md)
- ```
+ ```markdown
+ [Link to Related Page](/miscellaneous.md)
+ ```
+
## References
- This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet).
- The [Markdown Syntax Guide](https://daringfireball.net/projects/markdown/syntax) at Daring Fireball is an excellent resource for a detailed explanation of standard markdown.
- [Dillinger.io](http://dillinger.io) is a handy tool for testing standard markdown.
+[^1]: This link will be broken if you see this document from the Help page or docs.gitlab.com
+[^2]: This is my awesome footnote.
+
[markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md
[rouge]: http://rouge.jneen.net/ "Rouge website"
[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
-[^1]: This link will be broken if you see this document from the Help page or docs.gitlab.com
+[katex]: https://github.com/Khan/KaTeX "KaTeX website"
+[katex-subset]: https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX "Macros supported by KaTeX"
+[asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual" \ No newline at end of file
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index d6216a8dd50..39fe2409a29 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -32,6 +32,8 @@ The following table depicts the various user permission levels in a project.
| See a commit status | | ✓ | ✓ | ✓ | ✓ |
| See a container registry | | ✓ | ✓ | ✓ | ✓ |
| See environments | | ✓ | ✓ | ✓ | ✓ |
+| Create new environments | | | ✓ | ✓ | ✓ |
+| Stop environments | | | ✓ | ✓ | ✓ |
| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ |
| Manage/Accept merge requests | | | ✓ | ✓ | ✓ |
| Create new merge request | | | ✓ | ✓ | ✓ |
@@ -45,7 +47,6 @@ The following table depicts the various user permission levels in a project.
| Create or update commit status | | | ✓ | ✓ | ✓ |
| Update a container registry | | | ✓ | ✓ | ✓ |
| Remove a container registry image | | | ✓ | ✓ | ✓ |
-| Create new environments | | | ✓ | ✓ | ✓ |
| Create new milestones | | | | ✓ | ✓ |
| Add new team members | | | | ✓ | ✓ |
| Push to protected branches | | | | ✓ | ✓ |
@@ -58,7 +59,6 @@ The following table depicts the various user permission levels in a project.
| Manage runners | | | | ✓ | ✓ |
| Manage build triggers | | | | ✓ | ✓ |
| Manage variables | | | | ✓ | ✓ |
-| Delete environments | | | | ✓ | ✓ |
| Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ |
| Remove project | | | | | ✓ |
@@ -141,10 +141,9 @@ instance and project. In addition, all admins can use the admin interface under
| See events in the system | | | | ✓ |
| Admin interface | | | | ✓ |
-### Build permissions
-
-> Changed in GitLab 8.12.
+### Builds permissions
+>**Note:**
GitLab 8.12 has a completely redesigned build permissions system.
Read all about the [new model and its implications][new-mod].
diff --git a/doc/user/project/builds/img/build_artifacts_browser.png b/doc/user/project/builds/img/build_artifacts_browser.png
index d95e2800c0f..686273948d6 100644
--- a/doc/user/project/builds/img/build_artifacts_browser.png
+++ b/doc/user/project/builds/img/build_artifacts_browser.png
Binary files differ
diff --git a/doc/user/project/builds/img/build_artifacts_browser_button.png b/doc/user/project/builds/img/build_artifacts_browser_button.png
index 463540634e3..33ef7de0415 100644
--- a/doc/user/project/builds/img/build_artifacts_browser_button.png
+++ b/doc/user/project/builds/img/build_artifacts_browser_button.png
Binary files differ
diff --git a/doc/user/project/builds/img/build_artifacts_builds_page.png b/doc/user/project/builds/img/build_artifacts_builds_page.png
index db78386ba7b..8f75602d592 100644
--- a/doc/user/project/builds/img/build_artifacts_builds_page.png
+++ b/doc/user/project/builds/img/build_artifacts_builds_page.png
Binary files differ
diff --git a/doc/user/project/builds/img/build_artifacts_pipelines_page.png b/doc/user/project/builds/img/build_artifacts_pipelines_page.png
index 6c2d1a4bdc7..4bbd00ddaa0 100644
--- a/doc/user/project/builds/img/build_artifacts_pipelines_page.png
+++ b/doc/user/project/builds/img/build_artifacts_pipelines_page.png
Binary files differ
diff --git a/doc/user/project/builds/img/build_latest_artifacts_browser.png b/doc/user/project/builds/img/build_latest_artifacts_browser.png
index d8e9071958c..c6d8856078b 100644
--- a/doc/user/project/builds/img/build_latest_artifacts_browser.png
+++ b/doc/user/project/builds/img/build_latest_artifacts_browser.png
Binary files differ
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index b205fea2c40..47a4a3f85d0 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -4,13 +4,15 @@
---
-> **Note**
-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
-Registry across your GitLab instance, visit the
-[administrator documentation](../../administration/container_registry.md).
+>**Notes:**
+- 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
+ Registry across your GitLab instance, visit the
+ [administrator documentation](../../administration/container_registry.md).
+- Starting from GitLab 8.12, if you have 2FA enabled in your account, you need
+ to pass a personal access token instead of your password in order to login to
+ GitLab's Container Registry.
With the Docker Container Registry integrated into GitLab, every project can
have its own space to store its Docker images.
diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md
index 1892ccabb70..86fe52ef4ff 100644
--- a/doc/user/project/cycle_analytics.md
+++ b/doc/user/project/cycle_analytics.md
@@ -1,10 +1,7 @@
# Cycle Analytics
-> [Introduced][ce-5986] in GitLab 8.12.
->
-> **Note:**
-There are more changes coming to Cycle Analytics, you can follow the following
-issue to track the changes to this feature: [#20975][ce-20975].
+> [Introduced][ce-5986] in GitLab 8.12. Further features were added in GitLab
+ 8.14.
Cycle Analytics measures the time it takes to go from an [idea to production] for
each project you have. This is achieved by not only indicating the total time it
@@ -16,7 +13,7 @@ calculates a separate median for each stage.
## Overview
-You can find the Cycle Analytics page under your project's **Pipelines > Cycle
+You can find the Cycle Analytics page under your project's **Pipelines ➔ Cycle
Analytics** tab.
![Cycle Analytics landing page](img/cycle_analytics_landing_page.png)
diff --git a/doc/user/project/img/container_registry_enable.png b/doc/user/project/img/container_registry_enable.png
index 6fffa2a91d8..d067a8be1ca 100644
--- a/doc/user/project/img/container_registry_enable.png
+++ b/doc/user/project/img/container_registry_enable.png
Binary files differ
diff --git a/doc/user/project/img/container_registry_panel.png b/doc/user/project/img/container_registry_panel.png
index 60fd76192b7..e4c9ecbb25b 100644
--- a/doc/user/project/img/container_registry_panel.png
+++ b/doc/user/project/img/container_registry_panel.png
Binary files differ
diff --git a/doc/user/project/img/container_registry_tab.png b/doc/user/project/img/container_registry_tab.png
index 36b883aaa97..a85237271d9 100644
--- a/doc/user/project/img/container_registry_tab.png
+++ b/doc/user/project/img/container_registry_tab.png
Binary files differ
diff --git a/doc/user/project/img/cycle_analytics_landing_page.png b/doc/user/project/img/cycle_analytics_landing_page.png
index b212134d5ed..316612c0da0 100644
--- a/doc/user/project/img/cycle_analytics_landing_page.png
+++ b/doc/user/project/img/cycle_analytics_landing_page.png
Binary files differ
diff --git a/doc/user/project/img/description_templates.png b/doc/user/project/img/description_templates.png
index c41cc77a94c..e9d45029532 100644
--- a/doc/user/project/img/description_templates.png
+++ b/doc/user/project/img/description_templates.png
Binary files differ
diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png
index 63c269f6dbc..95e8532e908 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_add_list.png b/doc/user/project/img/issue_board_add_list.png
index 2b8c10eaa0a..cdfc466d23f 100644
--- a/doc/user/project/img/issue_board_add_list.png
+++ b/doc/user/project/img/issue_board_add_list.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
index 112ea171539..fbb67b9c18f 100644
--- a/doc/user/project/img/issue_board_search_backlog.png
+++ b/doc/user/project/img/issue_board_search_backlog.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_system_notes.png b/doc/user/project/img/issue_board_system_notes.png
index b69ef034954..bd0f5f54095 100644
--- a/doc/user/project/img/issue_board_system_notes.png
+++ b/doc/user/project/img/issue_board_system_notes.png
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 b757faeb230..5bfdac88dde 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/koding_build-in-progress.png b/doc/user/project/img/koding_build-in-progress.png
index f8cc81834c4..79b7b2f10a2 100644
--- a/doc/user/project/img/koding_build-in-progress.png
+++ b/doc/user/project/img/koding_build-in-progress.png
Binary files differ
diff --git a/doc/user/project/img/koding_build-logs.png b/doc/user/project/img/koding_build-logs.png
index a04cd5aff99..b30c8375b20 100644
--- a/doc/user/project/img/koding_build-logs.png
+++ b/doc/user/project/img/koding_build-logs.png
Binary files differ
diff --git a/doc/user/project/img/koding_build-success.png b/doc/user/project/img/koding_build-success.png
index 2a0dd296480..a2342cfd324 100644
--- a/doc/user/project/img/koding_build-success.png
+++ b/doc/user/project/img/koding_build-success.png
Binary files differ
diff --git a/doc/user/project/img/koding_commit-koding.yml.png b/doc/user/project/img/koding_commit-koding.yml.png
index 3e133c50327..16842410ae2 100644
--- a/doc/user/project/img/koding_commit-koding.yml.png
+++ b/doc/user/project/img/koding_commit-koding.yml.png
Binary files differ
diff --git a/doc/user/project/img/koding_different-stack-on-mr-try.png b/doc/user/project/img/koding_different-stack-on-mr-try.png
index fd25e32f648..10c7c51d2e6 100644
--- a/doc/user/project/img/koding_different-stack-on-mr-try.png
+++ b/doc/user/project/img/koding_different-stack-on-mr-try.png
Binary files differ
diff --git a/doc/user/project/img/koding_edit-on-ide.png b/doc/user/project/img/koding_edit-on-ide.png
index fd5aaff75f5..ab861281d3e 100644
--- a/doc/user/project/img/koding_edit-on-ide.png
+++ b/doc/user/project/img/koding_edit-on-ide.png
Binary files differ
diff --git a/doc/user/project/img/koding_enable-koding.png b/doc/user/project/img/koding_enable-koding.png
index c0ae0ee9918..0b6fcfadcc5 100644
--- a/doc/user/project/img/koding_enable-koding.png
+++ b/doc/user/project/img/koding_enable-koding.png
Binary files differ
diff --git a/doc/user/project/img/koding_landing.png b/doc/user/project/img/koding_landing.png
index 7c629d9b05e..1eeddcd3813 100644
--- a/doc/user/project/img/koding_landing.png
+++ b/doc/user/project/img/koding_landing.png
Binary files differ
diff --git a/doc/user/project/img/koding_open-gitlab-from-koding.png b/doc/user/project/img/koding_open-gitlab-from-koding.png
index c958cf8f224..4235a72b36f 100644
--- a/doc/user/project/img/koding_open-gitlab-from-koding.png
+++ b/doc/user/project/img/koding_open-gitlab-from-koding.png
Binary files differ
diff --git a/doc/user/project/img/koding_run-in-ide.png b/doc/user/project/img/koding_run-in-ide.png
index f91ee0f74cc..d22e5023c59 100644
--- a/doc/user/project/img/koding_run-in-ide.png
+++ b/doc/user/project/img/koding_run-in-ide.png
Binary files differ
diff --git a/doc/user/project/img/koding_run-mr-in-ide.png b/doc/user/project/img/koding_run-mr-in-ide.png
index 502817a2a46..cb1112c4034 100644
--- a/doc/user/project/img/koding_run-mr-in-ide.png
+++ b/doc/user/project/img/koding_run-mr-in-ide.png
Binary files differ
diff --git a/doc/user/project/img/koding_set-up-ide.png b/doc/user/project/img/koding_set-up-ide.png
index 7f408c980b5..033d41729a2 100644
--- a/doc/user/project/img/koding_set-up-ide.png
+++ b/doc/user/project/img/koding_set-up-ide.png
Binary files differ
diff --git a/doc/user/project/img/koding_stack-import.png b/doc/user/project/img/koding_stack-import.png
index 2a4e3c87fc8..245ccb07ba3 100644
--- a/doc/user/project/img/koding_stack-import.png
+++ b/doc/user/project/img/koding_stack-import.png
Binary files differ
diff --git a/doc/user/project/img/koding_start-build.png b/doc/user/project/img/koding_start-build.png
index 52159440f62..3f5c16d5d2f 100644
--- a/doc/user/project/img/koding_start-build.png
+++ b/doc/user/project/img/koding_start-build.png
Binary files differ
diff --git a/doc/user/project/img/labels_assign_label_in_new_issue.png b/doc/user/project/img/labels_assign_label_in_new_issue.png
index e32a35f7cda..badfbed0bbe 100644
--- a/doc/user/project/img/labels_assign_label_in_new_issue.png
+++ b/doc/user/project/img/labels_assign_label_in_new_issue.png
Binary files differ
diff --git a/doc/user/project/img/labels_assign_label_sidebar.png b/doc/user/project/img/labels_assign_label_sidebar.png
index 799443af889..d74796fdb4d 100644
--- a/doc/user/project/img/labels_assign_label_sidebar.png
+++ b/doc/user/project/img/labels_assign_label_sidebar.png
Binary files differ
diff --git a/doc/user/project/img/labels_assign_label_sidebar_saved.png b/doc/user/project/img/labels_assign_label_sidebar_saved.png
index e7d8d69e60e..dabffe956dc 100644
--- a/doc/user/project/img/labels_assign_label_sidebar_saved.png
+++ b/doc/user/project/img/labels_assign_label_sidebar_saved.png
Binary files differ
diff --git a/doc/user/project/img/labels_default.png b/doc/user/project/img/labels_default.png
index ee0c9f889ad..474953d565b 100644
--- a/doc/user/project/img/labels_default.png
+++ b/doc/user/project/img/labels_default.png
Binary files differ
diff --git a/doc/user/project/img/labels_description_tooltip.png b/doc/user/project/img/labels_description_tooltip.png
index 0d1e3e091fb..eea4f8cf0f4 100644
--- a/doc/user/project/img/labels_description_tooltip.png
+++ b/doc/user/project/img/labels_description_tooltip.png
Binary files differ
diff --git a/doc/user/project/img/labels_filter.png b/doc/user/project/img/labels_filter.png
index ed622be2d93..3aca77f0070 100644
--- a/doc/user/project/img/labels_filter.png
+++ b/doc/user/project/img/labels_filter.png
Binary files differ
diff --git a/doc/user/project/img/labels_filter_by_priority.png b/doc/user/project/img/labels_filter_by_priority.png
index c5a9e20919b..5609a1f6d7f 100644
--- a/doc/user/project/img/labels_filter_by_priority.png
+++ b/doc/user/project/img/labels_filter_by_priority.png
Binary files differ
diff --git a/doc/user/project/img/labels_generate.png b/doc/user/project/img/labels_generate.png
index 9579be4e231..987f4b5be71 100644
--- a/doc/user/project/img/labels_generate.png
+++ b/doc/user/project/img/labels_generate.png
Binary files differ
diff --git a/doc/user/project/img/labels_new_label.png b/doc/user/project/img/labels_new_label.png
index a916d3dceb5..b44b4bd296d 100644
--- a/doc/user/project/img/labels_new_label.png
+++ b/doc/user/project/img/labels_new_label.png
Binary files differ
diff --git a/doc/user/project/img/labels_new_label_on_the_fly.png b/doc/user/project/img/labels_new_label_on_the_fly.png
index 80cc434239e..2ac9805b1ab 100644
--- a/doc/user/project/img/labels_new_label_on_the_fly.png
+++ b/doc/user/project/img/labels_new_label_on_the_fly.png
Binary files differ
diff --git a/doc/user/project/img/labels_new_label_on_the_fly_create.png b/doc/user/project/img/labels_new_label_on_the_fly_create.png
index c41090945eb..02ccf68553b 100644
--- a/doc/user/project/img/labels_new_label_on_the_fly_create.png
+++ b/doc/user/project/img/labels_new_label_on_the_fly_create.png
Binary files differ
diff --git a/doc/user/project/img/labels_prioritize.png b/doc/user/project/img/labels_prioritize.png
index 8dfe72cf826..3e888f36364 100644
--- a/doc/user/project/img/labels_prioritize.png
+++ b/doc/user/project/img/labels_prioritize.png
Binary files differ
diff --git a/doc/user/project/img/labels_subscribe.png b/doc/user/project/img/labels_subscribe.png
index ea3db2bc0cf..56f24ae7bc8 100644
--- a/doc/user/project/img/labels_subscribe.png
+++ b/doc/user/project/img/labels_subscribe.png
Binary files differ
diff --git a/doc/user/project/img/mitmproxy-docker.png b/doc/user/project/img/mitmproxy-docker.png
index 4e3e37b413d..aa3b6a0b830 100644
--- a/doc/user/project/img/mitmproxy-docker.png
+++ b/doc/user/project/img/mitmproxy-docker.png
Binary files differ
diff --git a/doc/user/project/img/project_settings_list.png b/doc/user/project/img/project_settings_list.png
index cd9f5c00eea..0bb761b45c9 100644
--- a/doc/user/project/img/project_settings_list.png
+++ b/doc/user/project/img/project_settings_list.png
Binary files differ
diff --git a/doc/user/project/img/protected_branches_choose_branch.png b/doc/user/project/img/protected_branches_choose_branch.png
index 26328143717..c2848db9c96 100644
--- a/doc/user/project/img/protected_branches_choose_branch.png
+++ b/doc/user/project/img/protected_branches_choose_branch.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 812cc8767b7..1c05cb8fd36 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/img/protected_branches_error_ui.png b/doc/user/project/img/protected_branches_error_ui.png
index cc61df7ca97..3f8e462d3ad 100644
--- a/doc/user/project/img/protected_branches_error_ui.png
+++ b/doc/user/project/img/protected_branches_error_ui.png
Binary files differ
diff --git a/doc/user/project/img/protected_branches_list.png b/doc/user/project/img/protected_branches_list.png
index f33f1b2bdb6..1b2936cb711 100644
--- a/doc/user/project/img/protected_branches_list.png
+++ b/doc/user/project/img/protected_branches_list.png
Binary files differ
diff --git a/doc/user/project/img/protected_branches_matches.png b/doc/user/project/img/protected_branches_matches.png
index 30ce53f704e..d7f2c8582fc 100644
--- a/doc/user/project/img/protected_branches_matches.png
+++ b/doc/user/project/img/protected_branches_matches.png
Binary files differ
diff --git a/doc/user/project/img/protected_branches_page.png b/doc/user/project/img/protected_branches_page.png
index 1585dde5b29..4e5afff3bae 100644
--- a/doc/user/project/img/protected_branches_page.png
+++ b/doc/user/project/img/protected_branches_page.png
Binary files differ
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index 4a6c0d88241..d1ae57c00d7 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -72,7 +72,7 @@ the list will be created and filled with the issues that have that label.
## Creating a new list
-Create a new list by clicking on the **Create new list** button at the upper
+Create a new list by clicking on the **Add list** button at the upper
right corner of the Issue Board.
![Issue Board welcome message](img/issue_board_add_list.png)
diff --git a/doc/user/project/merge_requests.md b/doc/user/project/merge_requests.md
index 5af9a5d049c..be09337319f 100644
--- a/doc/user/project/merge_requests.md
+++ b/doc/user/project/merge_requests.md
@@ -19,14 +19,14 @@ in a merged merge requests or a commit.
[Learn more about cherry-picking changes.](merge_requests/cherry_pick_changes.md)
-## Merge when build succeeds
+## 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 all
-builds succeed. This way, you don't have to wait for the builds to finish and
-remember to merge the request manually.
+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 build succeeds.](merge_requests/merge_when_build_succeeds.md)
+[Learn more about merging when pipeline succeeds.](merge_requests/merge_when_pipeline_succeeds.md)
## Resolve discussion comments in merge requests reviews
diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png b/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png
index 7fb68cc9e9b..5ab094ab367 100644
--- a/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png
+++ b/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png b/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png
index 5267e04562f..42dcb9203ec 100644
--- a/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png
+++ b/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png b/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png
index 975fb13e463..71227747182 100644
--- a/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png
+++ b/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png b/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png
index 6c003bacbe3..604eb22f51c 100644
--- a/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png
+++ b/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/commit_compare.png b/doc/user/project/merge_requests/img/commit_compare.png
index 0e4a2b23c04..e612a39716e 100644
--- a/doc/user/project/merge_requests/img/commit_compare.png
+++ b/doc/user/project/merge_requests/img/commit_compare.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/conflict_section.png b/doc/user/project/merge_requests/img/conflict_section.png
index 842e50b14b2..cfc17013218 100644
--- a/doc/user/project/merge_requests/img/conflict_section.png
+++ b/doc/user/project/merge_requests/img/conflict_section.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/discussion_view.png b/doc/user/project/merge_requests/img/discussion_view.png
index 83bb60acce2..2ee1db2eab3 100644
--- a/doc/user/project/merge_requests/img/discussion_view.png
+++ b/doc/user/project/merge_requests/img/discussion_view.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/discussions_resolved.png b/doc/user/project/merge_requests/img/discussions_resolved.png
index 85428129ac8..3fd496f6da5 100644
--- a/doc/user/project/merge_requests/img/discussions_resolved.png
+++ b/doc/user/project/merge_requests/img/discussions_resolved.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_request_diff.png b/doc/user/project/merge_requests/img/merge_request_diff.png
index 06ee4908edc..9c5488cb207 100644
--- a/doc/user/project/merge_requests/img/merge_request_diff.png
+++ b/doc/user/project/merge_requests/img/merge_request_diff.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_request_widget.png b/doc/user/project/merge_requests/img/merge_request_widget.png
index ffb96b17b07..43a945c74d9 100644
--- a/doc/user/project/merge_requests/img/merge_request_widget.png
+++ b/doc/user/project/merge_requests/img/merge_request_widget.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png
index b86e6d7b3fd..f50a1be24f2 100644
--- a/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png
+++ b/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png
index 6b9756b7418..c43f76b058c 100644
--- a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png
+++ b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png
index 18bebf5fe92..ddc58ff2630 100644
--- a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png
+++ b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png
index f3ea61d8147..a98636ee359 100644
--- a/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png
+++ b/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved.png b/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved.png
index 52c8acf15e0..928c7d33898 100644
--- a/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved.png
+++ b/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved_msg.png b/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved_msg.png
index 79ba5c362c7..bcdc0250d7c 100644
--- a/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved_msg.png
+++ b/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved_msg.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/preview_issue_for_discussions.png b/doc/user/project/merge_requests/img/preview_issue_for_discussions.png
new file mode 100644
index 00000000000..9fdd387676c
--- /dev/null
+++ b/doc/user/project/merge_requests/img/preview_issue_for_discussions.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/resolve_comment_button.png b/doc/user/project/merge_requests/img/resolve_comment_button.png
index 2c4ab2f5d53..70340108874 100644
--- a/doc/user/project/merge_requests/img/resolve_comment_button.png
+++ b/doc/user/project/merge_requests/img/resolve_comment_button.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/resolve_discussion_button.png b/doc/user/project/merge_requests/img/resolve_discussion_button.png
index 73f265bb101..ab454f661e0 100644
--- a/doc/user/project/merge_requests/img/resolve_discussion_button.png
+++ b/doc/user/project/merge_requests/img/resolve_discussion_button.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/revert_changes_commit.png b/doc/user/project/merge_requests/img/revert_changes_commit.png
index e7194fc3504..a0663e130e9 100644
--- a/doc/user/project/merge_requests/img/revert_changes_commit.png
+++ b/doc/user/project/merge_requests/img/revert_changes_commit.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/revert_changes_commit_modal.png b/doc/user/project/merge_requests/img/revert_changes_commit_modal.png
index c660ec7eaec..ef7b6dae553 100644
--- a/doc/user/project/merge_requests/img/revert_changes_commit_modal.png
+++ b/doc/user/project/merge_requests/img/revert_changes_commit_modal.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/revert_changes_mr.png b/doc/user/project/merge_requests/img/revert_changes_mr.png
index 3002f0ac1c5..8792018ee53 100644
--- a/doc/user/project/merge_requests/img/revert_changes_mr.png
+++ b/doc/user/project/merge_requests/img/revert_changes_mr.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/revert_changes_mr_modal.png b/doc/user/project/merge_requests/img/revert_changes_mr_modal.png
index c6aaeecc8a6..f6540c9dd33 100644
--- a/doc/user/project/merge_requests/img/revert_changes_mr_modal.png
+++ b/doc/user/project/merge_requests/img/revert_changes_mr_modal.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/versions.png b/doc/user/project/merge_requests/img/versions.png
index 6c86f2c68ac..33c58d2abff 100644
--- a/doc/user/project/merge_requests/img/versions.png
+++ b/doc/user/project/merge_requests/img/versions.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/versions_compare.png b/doc/user/project/merge_requests/img/versions_compare.png
index 890cae7768c..db978ea7b1d 100644
--- a/doc/user/project/merge_requests/img/versions_compare.png
+++ b/doc/user/project/merge_requests/img/versions_compare.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/versions_dropdown.png b/doc/user/project/merge_requests/img/versions_dropdown.png
index 9bab9304e14..889a2d93e6c 100644
--- a/doc/user/project/merge_requests/img/versions_dropdown.png
+++ b/doc/user/project/merge_requests/img/versions_dropdown.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/versions_system_note.png b/doc/user/project/merge_requests/img/versions_system_note.png
index 7c9d7715745..90be6298d15 100644
--- a/doc/user/project/merge_requests/img/versions_system_note.png
+++ b/doc/user/project/merge_requests/img/versions_system_note.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/wip_blocked_accept_button.png b/doc/user/project/merge_requests/img/wip_blocked_accept_button.png
index 89c458aa8d9..047b0b4620f 100644
--- a/doc/user/project/merge_requests/img/wip_blocked_accept_button.png
+++ b/doc/user/project/merge_requests/img/wip_blocked_accept_button.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/wip_mark_as_wip.png b/doc/user/project/merge_requests/img/wip_mark_as_wip.png
index 9c37354a653..8bd206bc24a 100644
--- a/doc/user/project/merge_requests/img/wip_mark_as_wip.png
+++ b/doc/user/project/merge_requests/img/wip_mark_as_wip.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/wip_unmark_as_wip.png b/doc/user/project/merge_requests/img/wip_unmark_as_wip.png
index 31f7326beb0..c0bfa6a35a2 100644
--- a/doc/user/project/merge_requests/img/wip_unmark_as_wip.png
+++ b/doc/user/project/merge_requests/img/wip_unmark_as_wip.png
Binary files differ
diff --git a/doc/user/project/merge_requests/merge_request_discussion_resolution.md b/doc/user/project/merge_requests/merge_request_discussion_resolution.md
index 285b1798ac5..f37f1ce4d21 100644
--- a/doc/user/project/merge_requests/merge_request_discussion_resolution.md
+++ b/doc/user/project/merge_requests/merge_request_discussion_resolution.md
@@ -37,7 +37,8 @@ resolved discussions tracker.
> [Introduced][ce-7125] in GitLab 8.14.
-You can prevent merge requests from being merged until all discussions are resolved.
+You can prevent merge requests from being merged until all discussions are
+resolved.
Navigate to your project's settings page, select the
**Only allow merge requests to be merged if all discussions are resolved** check
@@ -50,8 +51,26 @@ are resolved.
![Only allow merge if all the discussions are resolved message](img/only_allow_merge_if_all_discussions_are_resolved_msg.png)
+### Move all unresolved discussions in a merge request to an issue
+
+> [Introduced][ce-7180] (Currently on Backlog)
+
+To delegate unresolved discussions to a new issue you can click the link **open
+an issue to resolve them later**.
+
+This will prepare an issue with content referring to the merge request and
+discussions.
+
+![Issue mentioning discussions in a merge request](img/preview_issue_for_discussions.png)
+
+Hitting **Submit issue** will cause all discussions to be marked as resolved and
+add a note referring to the newly created issue.
+
+You can now proceed to merge the merge request from the UI.
+
[ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022
[ce-7125]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7125
+[ce-7180]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7180
[resolve-discussion-button]: img/resolve_discussion_button.png
[resolve-comment-button]: img/resolve_comment_button.png
[discussion-view]: img/discussion_view.png
diff --git a/doc/user/project/merge_requests/merge_when_build_succeeds.md b/doc/user/project/merge_requests/merge_when_build_succeeds.md
index d4e5b5de685..2167fdfbf7e 100644
--- a/doc/user/project/merge_requests/merge_when_build_succeeds.md
+++ b/doc/user/project/merge_requests/merge_when_build_succeeds.md
@@ -1,46 +1,5 @@
-# Merge When Build Succeeds
+This document was moved to [merge_when_pipeline_succeeds](merge_when_pipeline_succeeds.md).
-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 the
-builds pipeline succeed. This way, you don't have to wait for the builds to
-finish and remember to merge the request manually.
+>[Introduced][ce-7135] by the "Rename MWBS service to Merge When Pipeline Succeeds" change.
-![Enable](img/merge_when_build_succeeds_enable.png)
-
-When you hit the "Merge When Build Succeeds" button, the status of the merge
-request will be updated to represent the impending merge. If you cannot wait
-for the pipeline to succeed and want to merge immediately, this option is
-available in the dropdown menu on the right of the main button.
-
-Both team developers and the author of the merge request have the option to
-cancel the automatic merge if they find a reason why it shouldn't be merged
-after all.
-
-![Status](img/merge_when_build_succeeds_status.png)
-
-When the pipeline succeeds, the merge request will automatically be merged.
-When the pipeline fails, the author gets a chance to retry any failed builds,
-or to push new commits to fix the failure.
-
-When the builds are retried and succeed on the second try, the merge request
-will automatically be merged after all. When the merge request is updated with
-new commits, the automatic merge is automatically canceled to allow the new
-changes to be reviewed.
-
-## Only allow merge requests to be merged if the build succeeds
-
-> **Note:**
-You need to have builds configured to enable this feature.
-
-You can prevent merge requests from being merged if their build did not succeed.
-
-Navigate to your project's settings page, select the
-**Only allow merge requests to be merged if the build succeeds** check box and
-hit **Save** for the changes to take effect.
-
-![Only allow merge if build succeeds settings](img/merge_when_build_succeeds_only_if_succeeds_settings.png)
-
-From now on, every time the pipeline fails you will not be able to merge the
-merge request from the UI, until you make all relevant builds pass.
-
-![Only allow merge if build succeeds message](img/merge_when_build_succeeds_only_if_succeeds_msg.png)
+[ce-7135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7135
diff --git a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
new file mode 100644
index 00000000000..75ad18b28cf
--- /dev/null
+++ b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
@@ -0,0 +1,46 @@
+# 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 the
+builds pipeline succeeds. This way, you don't have to wait for the builds to
+finish and remember to merge the request manually.
+
+![Enable](img/merge_when_build_succeeds_enable.png)
+
+When you hit the "Merge When Pipeline Succeeds" button, the status of the merge
+request will be updated to represent the impending merge. If you cannot wait
+for the pipeline to succeed and want to merge immediately, this option is
+available in the dropdown menu on the right of the main button.
+
+Both team developers and the author of the merge request have the option to
+cancel the automatic merge if they find a reason why it shouldn't be merged
+after all.
+
+![Status](img/merge_when_build_succeeds_status.png)
+
+When the pipeline succeeds, the merge request will automatically be merged.
+When the pipeline fails, the author gets a chance to retry any failed builds,
+or to push new commits to fix the failure.
+
+When the builds are retried and succeed on the second try, the merge request
+will automatically be merged after all. When the merge request is updated with
+new commits, the automatic merge is automatically canceled to allow the new
+changes to be reviewed.
+
+## Only allow merge requests to be merged if the pipeline succeeds
+
+> **Note:**
+You need to have builds configured to enable this feature.
+
+You can prevent merge requests from being merged if their pipeline did not succeed.
+
+Navigate to your project's settings page, select the
+**Only allow merge requests to be merged if the pipeline succeeds** check box and
+hit **Save** for the changes to take effect.
+
+![Only allow merge if pipeline succeeds settings](img/merge_when_build_succeeds_only_if_succeeds_settings.png)
+
+From now on, every time the pipeline fails you will not be able to merge the
+merge request from the UI, until you make all relevant builds pass.
+
+![Only allow merge if pipeline succeeds message](img/merge_when_build_succeeds_only_if_succeeds_msg.png)
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index 60b7bec2ba7..320faff65c5 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -34,9 +34,9 @@ as created be the pusher (local push or via the UI) and any build created in thi
pipeline will have the permissions of the pusher.
This allows us to make it really easy to evaluate the access for all projects
-that have Git submodules or are using container images that the pusher would
-have access too. **The permission is granted only for time that build is running.
-The access is revoked after the build is finished.**
+that have [Git submodules][gitsub] or are using container images that the pusher
+would have access too. **The permission is granted only for time that build is
+running. The access is revoked after the build is finished.**
## Types of users
@@ -141,7 +141,7 @@ with GitLab 8.12.
With the new build permissions model, there is now an easy way to access all
dependent source code in a project. That way, we can:
-1. Access a project's Git submodules
+1. Access a project's [Git submodules][gitsub]
1. Access private container images
1. Access project's and submodule LFS objects
@@ -179,119 +179,25 @@ As a user:
### Git submodules
->
-It often happens that while working on one project, you need to use another
-project from within it; perhaps it’s a library that a third party developed or
-you’re developing a project separately and are using it in multiple parent
-projects.
-A common issue arises in these scenarios: you want to be able to treat the two
-projects as separate yet still be able to use one from within the other.
->
-_Excerpt from the [Git website][git-scm] about submodules._
-
-If dealing with submodules, your project will probably have a file named
-`.gitmodules`. And this is how it usually looks like:
-
-```
-[submodule "tools"]
- path = tools
- url = git@gitlab.com/group/tools.git
-```
-
-> **Note:**
-If you are **not** using GitLab 8.12 or higher, you would need to work your way
-around this issue in order to access the sources of `gitlab.com/group/tools`
-(e.g., use [SSH keys](../ssh_keys/README.md)).
->
-With GitLab 8.12 onward, your permissions are used to evaluate what a CI build
-can access. More information about how this system works can be found in the
-[Build permissions model](../../user/permissions.md#builds-permissions).
-
-To make use of the new changes, you have to update your `.gitmodules` file to
-use a relative URL.
-
-Let's consider the following example:
-
-1. Your project is located at `https://gitlab.com/secret-group/my-project`.
-1. To checkout your sources you usually use an SSH address like
- `git@gitlab.com:secret-group/my-project.git`.
-1. Your project depends on `https://gitlab.com/group/tools`.
-1. You have the `.gitmodules` file with above content.
-
-Since Git allows the usage of relative URLs for your `.gitmodules` configuration,
-this easily allows you to use HTTP for cloning all your CI builds and SSH
-for all your local checkouts.
-
-For example, if you change the `url` of your `tools` dependency, from
-`git@gitlab.com/group/tools.git` to `../../group/tools.git`, this will instruct
-Git to automatically deduce the URL that should be used when cloning sources.
-Whether you use HTTP or SSH, Git will use that same channel and it will allow
-to make all your CI builds use HTTPS (because GitLab CI uses HTTPS for cloning
-your sources), and all your local clones will continue using SSH.
-
-Given the above explanation, your `.gitmodules` file should eventually look
-like this:
-
-```
-[submodule "tools"]
- path = tools
- url = ../../group/tools.git
-```
-
-However, you have to explicitly tell GitLab CI to clone your submodules as this
-is not done automatically. You can achieve that by adding a `before_script`
-section to your `.gitlab-ci.yml`:
-
-```
-before_script:
- - git submodule update --init --recursive
-
-test:
- script:
- - run-my-tests
-```
-
-This will make GitLab CI initialize (fetch) and update (checkout) all your
-submodules recursively.
-
-If Git does not use the newly added relative URLs but still uses your old URLs,
-you might need to add `git submodule sync --recursive` to your `.gitlab-ci.yml`,
-prior to running `git submodule update --init --recursive`. This transfers the
-changes from your `.gitmodules` file into the `.git` folder, which is kept by
-runners between runs.
-
-In case your environment or your Docker image doesn't have Git installed,
-you have to either ask your Administrator or install the missing dependency
-yourself:
-
-```
-# Debian / Ubuntu
-before_script:
- - apt-get update -y
- - apt-get install -y git-core
- - git submodule update --init --recursive
-
-# CentOS / RedHat
-before_script:
- - yum install git
- - git submodule update --init --recursive
-
-# Alpine
-before_script:
- - apk add -U git
- - git submodule update --init --recursive
-```
+To properly configure submodules with GitLab CI, read the
+[Git submodules documentation][gitsub].
### Container Registry
With the update permission model we also extended the support for accessing
Container Registries for private projects.
-> **Note:**
-As GitLab Runner 1.6 doesn't yet incorporate the introduced changes for
-permissions, this makes the `image:` directive to not work with private projects
-automatically. The manual configuration by an Administrator is required to use
-private images. We plan to remove that limitation in one of the upcoming releases.
+> **Notes:**
+- GitLab Runner versions prior to 1.8 don't incorporate the introduced changes
+ for permissions. This makes the `image:` directive to not work with private
+ projects automatically and it needs to be configured manually on Runner's host
+ with a predefined account (for example administrator's personal account with
+ access token created explicitly for this purpose). This issue is resolved with
+ latest changes in GitLab Runner 1.8 which receives GitLab credentials with
+ build data.
+- Starting with GitLab 8.12, if you have 2FA enabled in your account, you need
+ to pass a personal access token instead of your password in order to login to
+ GitLab's Container Registry.
Your builds can access all container images that you would normally have access
to. The only implication is that you can push to the Container Registry of the
@@ -310,7 +216,7 @@ test:
[build permissions]: ../permissions.md#builds-permissions
[comment]: https://gitlab.com/gitlab-org/gitlab-ce/issues/22484#note_16648302
[ext]: ../permissions.md#external-users
-[git-scm]: https://git-scm.com/book/en/v2/Git-Tools-Submodules
+[gitsub]: ../../ci/git_submodules.md
[https]: ../admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols
[triggers]: ../../ci/triggers/README.md
[update-docs]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update
diff --git a/doc/user/project/pipelines/img/pipelines_settings_badges.png b/doc/user/project/pipelines/img/pipelines_settings_badges.png
index d0c4640791d..3bdc6374c15 100644
--- a/doc/user/project/pipelines/img/pipelines_settings_badges.png
+++ b/doc/user/project/pipelines/img/pipelines_settings_badges.png
Binary files differ
diff --git a/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png b/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png
index d2a5568521f..2a99201e014 100644
--- a/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png
+++ b/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png
Binary files differ
diff --git a/doc/user/project/pipelines/img/pipelines_test_coverage_build.png b/doc/user/project/pipelines/img/pipelines_test_coverage_build.png
index 3823100daf2..7eaba1a256f 100644
--- a/doc/user/project/pipelines/img/pipelines_test_coverage_build.png
+++ b/doc/user/project/pipelines/img/pipelines_test_coverage_build.png
Binary files differ
diff --git a/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png b/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png
index c4f78803e69..c166bb8bec8 100644
--- a/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png
+++ b/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_new_branch_dropdown.png b/doc/user/project/repository/img/web_editor_new_branch_dropdown.png
index a8e635d2faf..31edb6bde3a 100644
--- a/doc/user/project/repository/img/web_editor_new_branch_dropdown.png
+++ b/doc/user/project/repository/img/web_editor_new_branch_dropdown.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_new_branch_from_issue.png b/doc/user/project/repository/img/web_editor_new_branch_from_issue.png
index b0a63ddf0ab..4729f5383c0 100644
--- a/doc/user/project/repository/img/web_editor_new_branch_from_issue.png
+++ b/doc/user/project/repository/img/web_editor_new_branch_from_issue.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_new_branch_page.png b/doc/user/project/repository/img/web_editor_new_branch_page.png
index 7f36b7faf63..8d82f981527 100644
--- a/doc/user/project/repository/img/web_editor_new_branch_page.png
+++ b/doc/user/project/repository/img/web_editor_new_branch_page.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_new_directory_dialog.png b/doc/user/project/repository/img/web_editor_new_directory_dialog.png
index d16e3c67116..1c9beff8849 100644
--- a/doc/user/project/repository/img/web_editor_new_directory_dialog.png
+++ b/doc/user/project/repository/img/web_editor_new_directory_dialog.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_new_directory_dropdown.png b/doc/user/project/repository/img/web_editor_new_directory_dropdown.png
index c8d77b16ee8..ede691f6f74 100644
--- a/doc/user/project/repository/img/web_editor_new_directory_dropdown.png
+++ b/doc/user/project/repository/img/web_editor_new_directory_dropdown.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_new_file_dropdown.png b/doc/user/project/repository/img/web_editor_new_file_dropdown.png
index 3fcb91c9b93..13a4d721039 100644
--- a/doc/user/project/repository/img/web_editor_new_file_dropdown.png
+++ b/doc/user/project/repository/img/web_editor_new_file_dropdown.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_new_file_editor.png b/doc/user/project/repository/img/web_editor_new_file_editor.png
index 21c340b9288..d0bcc69bf63 100644
--- a/doc/user/project/repository/img/web_editor_new_file_editor.png
+++ b/doc/user/project/repository/img/web_editor_new_file_editor.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_new_push_widget.png b/doc/user/project/repository/img/web_editor_new_push_widget.png
index c7738a4c930..77756876d4f 100644
--- a/doc/user/project/repository/img/web_editor_new_push_widget.png
+++ b/doc/user/project/repository/img/web_editor_new_push_widget.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_new_tag_dropdown.png b/doc/user/project/repository/img/web_editor_new_tag_dropdown.png
index ac7415009b3..b52d5cabdf2 100644
--- a/doc/user/project/repository/img/web_editor_new_tag_dropdown.png
+++ b/doc/user/project/repository/img/web_editor_new_tag_dropdown.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_new_tag_page.png b/doc/user/project/repository/img/web_editor_new_tag_page.png
index 231e1a13fc0..d6d9945397c 100644
--- a/doc/user/project/repository/img/web_editor_new_tag_page.png
+++ b/doc/user/project/repository/img/web_editor_new_tag_page.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_start_new_merge_request.png b/doc/user/project/repository/img/web_editor_start_new_merge_request.png
index 2755501dfd1..384e8320f15 100644
--- a/doc/user/project/repository/img/web_editor_start_new_merge_request.png
+++ b/doc/user/project/repository/img/web_editor_start_new_merge_request.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_template_dropdown_buttons.png b/doc/user/project/repository/img/web_editor_template_dropdown_buttons.png
index 4efc51cc423..f21183125f6 100644
--- a/doc/user/project/repository/img/web_editor_template_dropdown_buttons.png
+++ b/doc/user/project/repository/img/web_editor_template_dropdown_buttons.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_template_dropdown_first_file.png b/doc/user/project/repository/img/web_editor_template_dropdown_first_file.png
index 67190c58823..7f31c2a8887 100644
--- a/doc/user/project/repository/img/web_editor_template_dropdown_first_file.png
+++ b/doc/user/project/repository/img/web_editor_template_dropdown_first_file.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_template_dropdown_mit_license.png b/doc/user/project/repository/img/web_editor_template_dropdown_mit_license.png
index 47719113805..afd44d78959 100644
--- a/doc/user/project/repository/img/web_editor_template_dropdown_mit_license.png
+++ b/doc/user/project/repository/img/web_editor_template_dropdown_mit_license.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_upload_file_dialog.png b/doc/user/project/repository/img/web_editor_upload_file_dialog.png
index 9d6d8250bbe..04e951406ad 100644
--- a/doc/user/project/repository/img/web_editor_upload_file_dialog.png
+++ b/doc/user/project/repository/img/web_editor_upload_file_dialog.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_upload_file_dropdown.png b/doc/user/project/repository/img/web_editor_upload_file_dropdown.png
index 6b5205b05ec..b8c766d4b99 100644
--- a/doc/user/project/repository/img/web_editor_upload_file_dropdown.png
+++ b/doc/user/project/repository/img/web_editor_upload_file_dropdown.png
Binary files differ
diff --git a/doc/user/project/settings/img/import_export_download_export.png b/doc/user/project/settings/img/import_export_download_export.png
index a2f7f0085c1..4945590e3e8 100644
--- a/doc/user/project/settings/img/import_export_download_export.png
+++ b/doc/user/project/settings/img/import_export_download_export.png
Binary files differ
diff --git a/doc/user/project/settings/img/import_export_export_button.png b/doc/user/project/settings/img/import_export_export_button.png
index 1f7bdd21b0d..eef79821f8b 100644
--- a/doc/user/project/settings/img/import_export_export_button.png
+++ b/doc/user/project/settings/img/import_export_export_button.png
Binary files differ
diff --git a/doc/user/project/settings/img/import_export_mail_link.png b/doc/user/project/settings/img/import_export_mail_link.png
index c123f83eb8e..48ef42855bc 100644
--- a/doc/user/project/settings/img/import_export_mail_link.png
+++ b/doc/user/project/settings/img/import_export_mail_link.png
Binary files differ
diff --git a/doc/user/project/settings/img/import_export_new_project.png b/doc/user/project/settings/img/import_export_new_project.png
index b3a7f201018..9dd509dc4a0 100644
--- a/doc/user/project/settings/img/import_export_new_project.png
+++ b/doc/user/project/settings/img/import_export_new_project.png
Binary files differ
diff --git a/doc/user/project/settings/img/import_export_select_file.png b/doc/user/project/settings/img/import_export_select_file.png
index f31832af3e1..fb831dca32b 100644
--- a/doc/user/project/settings/img/import_export_select_file.png
+++ b/doc/user/project/settings/img/import_export_select_file.png
Binary files differ
diff --git a/doc/user/project/settings/img/settings_edit_button.png b/doc/user/project/settings/img/settings_edit_button.png
index 3c0cee536de..9f3a8330e3a 100644
--- a/doc/user/project/settings/img/settings_edit_button.png
+++ b/doc/user/project/settings/img/settings_edit_button.png
Binary files differ
diff --git a/doc/web_hooks/ssl.png b/doc/web_hooks/ssl.png
index 8c4f08d1825..21ddec4ebdf 100644
--- a/doc/web_hooks/ssl.png
+++ b/doc/web_hooks/ssl.png
Binary files differ
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index 33c1a79d59c..1659dd1f6cb 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -1,17 +1,21 @@
# Webhooks
-_**Note:**
-Starting from GitLab 8.5:_
+>**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
-- _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.
-Project webhooks allow you to trigger an URL if new code is pushed or a new issue is created.
+Webhooks can be used to update an external issue tracker, trigger CI builds,
+update a backup mirror, or even deploy to your production server.
-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
@@ -26,21 +30,27 @@ GitLab webhooks keep in mind the following things:
you are writing a low-level hook this is important to remember.
- GitLab ignores the HTTP status code returned by your endpoint.
-## Secret Token
+## 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.
+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
+## 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.
+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)
-## Push events
+## Events
+
+Below are described the supported events.
+
+### Push events
Triggered when you push to the repository except when pushing tags.
@@ -121,7 +131,7 @@ X-Gitlab-Event: Push Hook
}
```
-## Tag events
+### Tag events
Triggered when you create (or delete) tags to the repository.
@@ -174,7 +184,7 @@ X-Gitlab-Event: Tag Push Hook
}
```
-## Issues events
+### Issues events
Triggered when a new issue is created or an existing issue was updated/closed/reopened.
@@ -240,7 +250,7 @@ X-Gitlab-Event: Issue Hook
}
}
```
-## Comment events
+### 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
@@ -253,7 +263,7 @@ Valid target types:
3. `issue`
4. `snippet`
-### Comment on commit
+#### Comment on commit
**Request header**:
@@ -332,7 +342,7 @@ X-Gitlab-Event: Note Hook
}
```
-### Comment on merge request
+#### Comment on merge request
**Request header**:
@@ -459,7 +469,7 @@ X-Gitlab-Event: Note Hook
}
```
-### Comment on issue
+#### Comment on issue
**Request header**:
@@ -534,7 +544,7 @@ X-Gitlab-Event: Note Hook
}
```
-### Comment on code snippet
+#### Comment on code snippet
**Request header**:
@@ -607,7 +617,7 @@ X-Gitlab-Event: Note Hook
}
```
-## Merge request events
+### 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.
@@ -699,7 +709,7 @@ X-Gitlab-Event: Merge Request Hook
}
```
-## Wiki Page events
+### Wiki Page events
Triggered when a wiki page is created or edited.
@@ -737,9 +747,9 @@ X-Gitlab-Event: Wiki Page Hook
},
"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",
+ "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": {
@@ -754,7 +764,7 @@ X-Gitlab-Event: Wiki Page Hook
}
```
-## Pipeline events
+### Pipeline events
Triggered on status change of Pipeline.
@@ -922,12 +932,70 @@ X-Gitlab-Event: Pipeline Hook
}
```
-#### Example webhook receiver
+### 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.
+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`.
+Save the following file as `print_http_body.rb`:
```ruby
require 'webrick'
@@ -947,7 +1015,8 @@ 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.
+When you press 'Test Hook' in GitLab, you should see something like this in the
+console:
```
{"before":"077a85dd266e6f3573ef7e9ef8ce3343ad659c4e","after":"95cd4a99e93bc4bbabacfa2cd10e6725b1403c60",<SNIP>}
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 2d9bfbc0629..59a806de210 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -25,12 +25,12 @@
- [Merge Requests](../user/project/merge_requests.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 build succeeds](../user/project/merge_requests/merge_when_build_succeeds.md)
+ - [Merge when pipeline succeeds](../user/project/merge_requests/merge_when_pipeline_succeeds.md)
- [Resolve discussion comments in merge requests reviews](../user/project/merge_requests/merge_request_discussion_resolution.md)
- [Resolve merge conflicts in the UI](../user/project/merge_requests/resolve_conflicts.md)
- [Revert changes in the UI](../user/project/merge_requests/revert_changes.md)
- [Merge requests versions](../user/project/merge_requests/versions.md)
- ["Work In Progress" merge requests](../user/project/merge_requests/work_in_progress_merge_requests.md)
- [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md)
-- [Importing from SVN, GitHub, BitBucket, etc](importing/README.md)
+- [Importing from SVN, GitHub, Bitbucket, etc](importing/README.md)
- [Todos](todos.md)
diff --git a/doc/workflow/add-user/img/access_requests_management.png b/doc/workflow/add-user/img/access_requests_management.png
index 5c9b510ba9d..3693bed869b 100644
--- a/doc/workflow/add-user/img/access_requests_management.png
+++ b/doc/workflow/add-user/img/access_requests_management.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_new_user_to_project_settings.png b/doc/workflow/add-user/img/add_new_user_to_project_settings.png
index 5da0552f9d6..40db600455f 100644
--- a/doc/workflow/add-user/img/add_new_user_to_project_settings.png
+++ b/doc/workflow/add-user/img/add_new_user_to_project_settings.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_email_accept.png b/doc/workflow/add-user/img/add_user_email_accept.png
index a2954ad7c37..763b3ff463d 100644
--- a/doc/workflow/add-user/img/add_user_email_accept.png
+++ b/doc/workflow/add-user/img/add_user_email_accept.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_email_ready.png b/doc/workflow/add-user/img/add_user_email_ready.png
index 19d91bc0999..0066eb3427b 100644
--- a/doc/workflow/add-user/img/add_user_email_ready.png
+++ b/doc/workflow/add-user/img/add_user_email_ready.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_email_search.png b/doc/workflow/add-user/img/add_user_email_search.png
index cb31b77d941..66bcd6aad80 100644
--- a/doc/workflow/add-user/img/add_user_email_search.png
+++ b/doc/workflow/add-user/img/add_user_email_search.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_give_permissions.png b/doc/workflow/add-user/img/add_user_give_permissions.png
index e6b77022f06..376a3eefccc 100644
--- a/doc/workflow/add-user/img/add_user_give_permissions.png
+++ b/doc/workflow/add-user/img/add_user_give_permissions.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_import_members_from_another_project.png b/doc/workflow/add-user/img/add_user_import_members_from_another_project.png
index 1068589c5ff..0c32001098e 100644
--- a/doc/workflow/add-user/img/add_user_import_members_from_another_project.png
+++ b/doc/workflow/add-user/img/add_user_import_members_from_another_project.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_imported_members.png b/doc/workflow/add-user/img/add_user_imported_members.png
index 5cd120a4245..51fd7688890 100644
--- a/doc/workflow/add-user/img/add_user_imported_members.png
+++ b/doc/workflow/add-user/img/add_user_imported_members.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_list_members.png b/doc/workflow/add-user/img/add_user_list_members.png
index 5fe3482192e..e0fa404288d 100644
--- a/doc/workflow/add-user/img/add_user_list_members.png
+++ b/doc/workflow/add-user/img/add_user_list_members.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_members_menu.png b/doc/workflow/add-user/img/add_user_members_menu.png
index 340d15c9830..8e61d15fe65 100644
--- a/doc/workflow/add-user/img/add_user_members_menu.png
+++ b/doc/workflow/add-user/img/add_user_members_menu.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_search_people.png b/doc/workflow/add-user/img/add_user_search_people.png
index 1c05d70ca31..41767a9167c 100644
--- a/doc/workflow/add-user/img/add_user_search_people.png
+++ b/doc/workflow/add-user/img/add_user_search_people.png
Binary files differ
diff --git a/doc/workflow/add-user/img/request_access_button.png b/doc/workflow/add-user/img/request_access_button.png
index 984d640b0f0..608baccb0ca 100644
--- a/doc/workflow/add-user/img/request_access_button.png
+++ b/doc/workflow/add-user/img/request_access_button.png
Binary files differ
diff --git a/doc/workflow/add-user/img/withdraw_access_request_button.png b/doc/workflow/add-user/img/withdraw_access_request_button.png
index ff54a0e4384..6edd786b151 100644
--- a/doc/workflow/add-user/img/withdraw_access_request_button.png
+++ b/doc/workflow/add-user/img/withdraw_access_request_button.png
Binary files differ
diff --git a/doc/workflow/award_emoji.png b/doc/workflow/award_emoji.png
index 481680af80c..1ad634a343e 100644
--- a/doc/workflow/award_emoji.png
+++ b/doc/workflow/award_emoji.png
Binary files differ
diff --git a/doc/workflow/ci_mr.png b/doc/workflow/ci_mr.png
index f8a7708643e..77423c68190 100644
--- a/doc/workflow/ci_mr.png
+++ b/doc/workflow/ci_mr.png
Binary files differ
diff --git a/doc/workflow/close_issue_mr.png b/doc/workflow/close_issue_mr.png
index 5e520240233..70de2fb6cee 100644
--- a/doc/workflow/close_issue_mr.png
+++ b/doc/workflow/close_issue_mr.png
Binary files differ
diff --git a/doc/workflow/environment_branches.png b/doc/workflow/environment_branches.png
index 13fb0478eaa..0941a4cad9c 100644
--- a/doc/workflow/environment_branches.png
+++ b/doc/workflow/environment_branches.png
Binary files differ
diff --git a/doc/workflow/forking/branch_select.png b/doc/workflow/forking/branch_select.png
index 7f19414f3a9..3e82afca75b 100644
--- a/doc/workflow/forking/branch_select.png
+++ b/doc/workflow/forking/branch_select.png
Binary files differ
diff --git a/doc/workflow/forking/merge_request.png b/doc/workflow/forking/merge_request.png
index e2da42a2be7..294775e1fdd 100644
--- a/doc/workflow/forking/merge_request.png
+++ b/doc/workflow/forking/merge_request.png
Binary files differ
diff --git a/doc/workflow/four_stages.png b/doc/workflow/four_stages.png
index 49413087dca..3ef6a33d2d4 100644
--- a/doc/workflow/four_stages.png
+++ b/doc/workflow/four_stages.png
Binary files differ
diff --git a/doc/workflow/git_pull.png b/doc/workflow/git_pull.png
index 9a1fdf899bf..2dd06b56c56 100644
--- a/doc/workflow/git_pull.png
+++ b/doc/workflow/git_pull.png
Binary files differ
diff --git a/doc/workflow/gitdashflow.png b/doc/workflow/gitdashflow.png
index e456cf9309d..65900853d84 100644
--- a/doc/workflow/gitdashflow.png
+++ b/doc/workflow/gitdashflow.png
Binary files differ
diff --git a/doc/workflow/github_flow.png b/doc/workflow/github_flow.png
index b3fca97cc2d..21a22becdb6 100644
--- a/doc/workflow/github_flow.png
+++ b/doc/workflow/github_flow.png
Binary files differ
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 2215f37b81a..c228ea72f22 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -279,7 +279,7 @@ The trick is to use the merge/pull request with multiple commits when your work
The commit message should reflect your intention, not the contents of the commit.
The contents of the commit can be easily seen anyway, the question is why you did it.
An example of a good commit message is: "Combine templates to dry up the user views.".
-Some words that are bad commit messages because they don't contain munch information are: change, improve and refactor.
+Some words that are bad commit messages because they don't contain much information are: change, improve and refactor.
The word fix or fixes is also a red flag, unless it comes after the commit sentence and references an issue number.
To see more information about the formatting of commit messages please see this great [blog post by Tim Pope](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
diff --git a/doc/workflow/gitlab_flow.png b/doc/workflow/gitlab_flow.png
index d85d4ff374e..c3562cc69a8 100644
--- a/doc/workflow/gitlab_flow.png
+++ b/doc/workflow/gitlab_flow.png
Binary files differ
diff --git a/doc/workflow/good_commit.png b/doc/workflow/good_commit.png
index 7958feea4d9..c3664aa97f2 100644
--- a/doc/workflow/good_commit.png
+++ b/doc/workflow/good_commit.png
Binary files differ
diff --git a/doc/workflow/groups/access_requests_management.png b/doc/workflow/groups/access_requests_management.png
index 5202434f00f..36deaa89a70 100644
--- a/doc/workflow/groups/access_requests_management.png
+++ b/doc/workflow/groups/access_requests_management.png
Binary files differ
diff --git a/doc/workflow/groups/add_member_to_group.png b/doc/workflow/groups/add_member_to_group.png
index 6e3f660d2e4..a10d5032bb0 100644
--- a/doc/workflow/groups/add_member_to_group.png
+++ b/doc/workflow/groups/add_member_to_group.png
Binary files differ
diff --git a/doc/workflow/groups/group_dashboard.png b/doc/workflow/groups/group_dashboard.png
index 662c932e536..a5829f25808 100644
--- a/doc/workflow/groups/group_dashboard.png
+++ b/doc/workflow/groups/group_dashboard.png
Binary files differ
diff --git a/doc/workflow/groups/group_with_two_projects.png b/doc/workflow/groups/group_with_two_projects.png
index dc3475949f5..76d0a1b8ab2 100644
--- a/doc/workflow/groups/group_with_two_projects.png
+++ b/doc/workflow/groups/group_with_two_projects.png
Binary files differ
diff --git a/doc/workflow/groups/max_access_level.png b/doc/workflow/groups/max_access_level.png
index 2855a514013..63f33f9d91d 100644
--- a/doc/workflow/groups/max_access_level.png
+++ b/doc/workflow/groups/max_access_level.png
Binary files differ
diff --git a/doc/workflow/groups/new_group_button.png b/doc/workflow/groups/new_group_button.png
index 26136312c8f..7155d6280bd 100644
--- a/doc/workflow/groups/new_group_button.png
+++ b/doc/workflow/groups/new_group_button.png
Binary files differ
diff --git a/doc/workflow/groups/new_group_form.png b/doc/workflow/groups/new_group_form.png
index dc50a069ef2..0d798cd4b84 100644
--- a/doc/workflow/groups/new_group_form.png
+++ b/doc/workflow/groups/new_group_form.png
Binary files differ
diff --git a/doc/workflow/groups/other_group_sees_shared_project.png b/doc/workflow/groups/other_group_sees_shared_project.png
index 2230720cecd..67af27043eb 100644
--- a/doc/workflow/groups/other_group_sees_shared_project.png
+++ b/doc/workflow/groups/other_group_sees_shared_project.png
Binary files differ
diff --git a/doc/workflow/groups/override_access_level.png b/doc/workflow/groups/override_access_level.png
index 9d6aaf4c363..2b3e9a49842 100644
--- a/doc/workflow/groups/override_access_level.png
+++ b/doc/workflow/groups/override_access_level.png
Binary files differ
diff --git a/doc/workflow/groups/project_members_via_group.png b/doc/workflow/groups/project_members_via_group.png
index 58270936a0b..878c9a03ac9 100644
--- a/doc/workflow/groups/project_members_via_group.png
+++ b/doc/workflow/groups/project_members_via_group.png
Binary files differ
diff --git a/doc/workflow/groups/request_access_button.png b/doc/workflow/groups/request_access_button.png
index 0eec5cb937d..f1aae6afed7 100644
--- a/doc/workflow/groups/request_access_button.png
+++ b/doc/workflow/groups/request_access_button.png
Binary files differ
diff --git a/doc/workflow/groups/share_project_with_groups.png b/doc/workflow/groups/share_project_with_groups.png
index 5772d4deced..3cb4796f9f7 100644
--- a/doc/workflow/groups/share_project_with_groups.png
+++ b/doc/workflow/groups/share_project_with_groups.png
Binary files differ
diff --git a/doc/workflow/groups/transfer_project.png b/doc/workflow/groups/transfer_project.png
index 0aef3ab3f0f..52161817f11 100644
--- a/doc/workflow/groups/transfer_project.png
+++ b/doc/workflow/groups/transfer_project.png
Binary files differ
diff --git a/doc/workflow/groups/withdraw_access_request_button.png b/doc/workflow/groups/withdraw_access_request_button.png
index b7de830a780..c5d8ef6c04f 100644
--- a/doc/workflow/groups/withdraw_access_request_button.png
+++ b/doc/workflow/groups/withdraw_access_request_button.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_comment_awarded.png b/doc/workflow/img/award_emoji_comment_awarded.png
index 67697831869..111793ebf8a 100644
--- a/doc/workflow/img/award_emoji_comment_awarded.png
+++ b/doc/workflow/img/award_emoji_comment_awarded.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_comment_picker.png b/doc/workflow/img/award_emoji_comment_picker.png
index d9c3faecdca..3ad1bab3119 100644
--- a/doc/workflow/img/award_emoji_comment_picker.png
+++ b/doc/workflow/img/award_emoji_comment_picker.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_select.png b/doc/workflow/img/award_emoji_select.png
index ad664c0aeff..e1b37beaf62 100644
--- a/doc/workflow/img/award_emoji_select.png
+++ b/doc/workflow/img/award_emoji_select.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_votes_least_popular.png b/doc/workflow/img/award_emoji_votes_least_popular.png
index 57d595d9602..86ede4b0c10 100644
--- a/doc/workflow/img/award_emoji_votes_least_popular.png
+++ b/doc/workflow/img/award_emoji_votes_least_popular.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_votes_most_popular.png b/doc/workflow/img/award_emoji_votes_most_popular.png
index 432bd09b8a7..1d3e2e57aa0 100644
--- a/doc/workflow/img/award_emoji_votes_most_popular.png
+++ b/doc/workflow/img/award_emoji_votes_most_popular.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_votes_sort_options.png b/doc/workflow/img/award_emoji_votes_sort_options.png
index ae6e224b317..c6dc1b939c1 100644
--- a/doc/workflow/img/award_emoji_votes_sort_options.png
+++ b/doc/workflow/img/award_emoji_votes_sort_options.png
Binary files differ
diff --git a/doc/workflow/img/file_finder_find_button.png b/doc/workflow/img/file_finder_find_button.png
index 96e383f0213..23139cc00c5 100644
--- a/doc/workflow/img/file_finder_find_button.png
+++ b/doc/workflow/img/file_finder_find_button.png
Binary files differ
diff --git a/doc/workflow/img/file_finder_find_file.png b/doc/workflow/img/file_finder_find_file.png
index c6508514c76..c2212c7cd9e 100644
--- a/doc/workflow/img/file_finder_find_file.png
+++ b/doc/workflow/img/file_finder_find_file.png
Binary files differ
diff --git a/doc/workflow/img/forking_workflow_choose_namespace.png b/doc/workflow/img/forking_workflow_choose_namespace.png
index 1839d5e8be2..b34b12090a1 100644
--- a/doc/workflow/img/forking_workflow_choose_namespace.png
+++ b/doc/workflow/img/forking_workflow_choose_namespace.png
Binary files differ
diff --git a/doc/workflow/img/forking_workflow_fork_button.png b/doc/workflow/img/forking_workflow_fork_button.png
index cc79d6fd40c..29854e6c516 100644
--- a/doc/workflow/img/forking_workflow_fork_button.png
+++ b/doc/workflow/img/forking_workflow_fork_button.png
Binary files differ
diff --git a/doc/workflow/img/forking_workflow_path_taken_error.png b/doc/workflow/img/forking_workflow_path_taken_error.png
index a859155aef0..9365fd13200 100644
--- a/doc/workflow/img/forking_workflow_path_taken_error.png
+++ b/doc/workflow/img/forking_workflow_path_taken_error.png
Binary files differ
diff --git a/doc/workflow/img/new_branch_from_issue.png b/doc/workflow/img/new_branch_from_issue.png
index 61acdd30ae9..286d775bb9e 100644
--- a/doc/workflow/img/new_branch_from_issue.png
+++ b/doc/workflow/img/new_branch_from_issue.png
Binary files differ
diff --git a/doc/workflow/img/todo_list_item.png b/doc/workflow/img/todo_list_item.png
index 884ba1d22a3..076069b651e 100644
--- a/doc/workflow/img/todo_list_item.png
+++ b/doc/workflow/img/todo_list_item.png
Binary files differ
diff --git a/doc/workflow/img/todos_add_todo_sidebar.png b/doc/workflow/img/todos_add_todo_sidebar.png
index 126ecc2c82f..3fa37067d1e 100644
--- a/doc/workflow/img/todos_add_todo_sidebar.png
+++ b/doc/workflow/img/todos_add_todo_sidebar.png
Binary files differ
diff --git a/doc/workflow/img/todos_icon.png b/doc/workflow/img/todos_icon.png
index bba77f88913..1ed16b09669 100644
--- a/doc/workflow/img/todos_icon.png
+++ b/doc/workflow/img/todos_icon.png
Binary files differ
diff --git a/doc/workflow/img/todos_index.png b/doc/workflow/img/todos_index.png
index f1438ef7355..902a5aa6bd3 100644
--- a/doc/workflow/img/todos_index.png
+++ b/doc/workflow/img/todos_index.png
Binary files differ
diff --git a/doc/workflow/img/todos_mark_done_sidebar.png b/doc/workflow/img/todos_mark_done_sidebar.png
index f449f977dd6..a8e756a71db 100644
--- a/doc/workflow/img/todos_mark_done_sidebar.png
+++ b/doc/workflow/img/todos_mark_done_sidebar.png
Binary files differ
diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png
deleted file mode 100644
index df55a081803..00000000000
--- a/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png
deleted file mode 100644
index 5253889d251..00000000000
--- a/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png
deleted file mode 100644
index ffa87ce5b2e..00000000000
--- a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png
deleted file mode 100644
index 1a5661de75d..00000000000
--- a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png
index fd7a4d3fabf..62c5c86c9b3 100644
--- a/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png
+++ b/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png
Binary files differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png
index fd1ba6f5884..96bce70b74d 100644
--- a/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png
+++ b/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png
Binary files differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png
index 186c1563951..b26c652e382 100644
--- a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png
+++ b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png
Binary files differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png
index 2f84d3232f2..ccc82f9d4cd 100644
--- a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png
+++ b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png
Binary files differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png
index 652ca20b9ab..28ff55a8d89 100644
--- a/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png
+++ b/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png
Binary files differ
diff --git a/doc/workflow/importing/gitlab_importer/importer.png b/doc/workflow/importing/gitlab_importer/importer.png
index 35a7ddc8318..27d42eb492e 100644
--- a/doc/workflow/importing/gitlab_importer/importer.png
+++ b/doc/workflow/importing/gitlab_importer/importer.png
Binary files differ
diff --git a/doc/workflow/importing/gitlab_importer/new_project_page.png b/doc/workflow/importing/gitlab_importer/new_project_page.png
index 81074d2d016..c673724f436 100644
--- a/doc/workflow/importing/gitlab_importer/new_project_page.png
+++ b/doc/workflow/importing/gitlab_importer/new_project_page.png
Binary files differ
diff --git a/doc/workflow/importing/img/bitbucket_import_grant_access.png b/doc/workflow/importing/img/bitbucket_import_grant_access.png
new file mode 100644
index 00000000000..429904e621d
--- /dev/null
+++ b/doc/workflow/importing/img/bitbucket_import_grant_access.png
Binary files differ
diff --git a/doc/workflow/importing/img/bitbucket_import_new_project.png b/doc/workflow/importing/img/bitbucket_import_new_project.png
new file mode 100644
index 00000000000..8ed528c2f09
--- /dev/null
+++ b/doc/workflow/importing/img/bitbucket_import_new_project.png
Binary files differ
diff --git a/doc/workflow/importing/img/bitbucket_import_select_project.png b/doc/workflow/importing/img/bitbucket_import_select_project.png
new file mode 100644
index 00000000000..1bca6166ec8
--- /dev/null
+++ b/doc/workflow/importing/img/bitbucket_import_select_project.png
Binary files differ
diff --git a/doc/workflow/importing/img/import_projects_from_github_importer.png b/doc/workflow/importing/img/import_projects_from_github_importer.png
index eadd33c695f..d8effaf6075 100644
--- a/doc/workflow/importing/img/import_projects_from_github_importer.png
+++ b/doc/workflow/importing/img/import_projects_from_github_importer.png
Binary files differ
diff --git a/doc/workflow/importing/img/import_projects_from_github_new_project_page.png b/doc/workflow/importing/img/import_projects_from_github_new_project_page.png
deleted file mode 100644
index 6e91c430a33..00000000000
--- a/doc/workflow/importing/img/import_projects_from_github_new_project_page.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png b/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png
index c11863ab10c..1ccb38a815e 100644
--- a/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png
+++ b/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png
Binary files differ
diff --git a/doc/workflow/importing/img/import_projects_from_new_project_page.png b/doc/workflow/importing/img/import_projects_from_new_project_page.png
new file mode 100644
index 00000000000..97ca30b2087
--- /dev/null
+++ b/doc/workflow/importing/img/import_projects_from_new_project_page.png
Binary files differ
diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md
index 520c4216295..b6d47e5afa2 100644
--- a/doc/workflow/importing/import_projects_from_bitbucket.md
+++ b/doc/workflow/importing/import_projects_from_bitbucket.md
@@ -1,26 +1,61 @@
# Import your project from Bitbucket to GitLab
-It takes just a few steps to import your existing Bitbucket projects to GitLab. But keep in mind that it is possible only if Bitbucket support is enabled on your GitLab instance. You can read more about Bitbucket support [here](../../integration/bitbucket.md).
+Import your projects from Bitbucket to GitLab with minimal effort.
-* Sign in to GitLab.com and go to your dashboard
+## Overview
-* Click on "New project"
+>**Note:**
+The [Bitbucket integration][bb-import] must be first enabled in order to be
+able to import your projects from Bitbucket. Ask your GitLab administrator
+to enable this if not already.
-![New project in GitLab](bitbucket_importer/bitbucket_import_new_project.png)
+- At its current state, the Bitbucket importer can import:
+ - the repository description (GitLab 7.7+)
+ - the Git repository data (GitLab 7.7+)
+ - the issues (GitLab 7.7+)
+ - the issue comments (GitLab 8.15+)
+ - the pull requests (GitLab 8.4+)
+ - the pull request comments (GitLab 8.15+)
+ - the milestones (GitLab 8.15+)
+- References to pull requests and issues are preserved (GitLab 8.7+)
+- Repository public access is retained. If a repository is private in Bitbucket
+ it will be created as private in GitLab as well.
-* Click on the "Bitbucket" button
-![Bitbucket](bitbucket_importer/bitbucket_import_select_bitbucket.png)
+## How it works
-* Grant GitLab access to your Bitbucket account
+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
+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.
-![Grant access](bitbucket_importer/bitbucket_import_grant_access.png)
+The importer will create any new namespaces (groups) if they don't exist or in
+the case the namespace is taken, the repository will be imported under the user's
+namespace that started the import process.
-* Click on the projects that you'd like to import or "Import all projects"
+## Importing your Bitbucket repositories
-![Import projects](bitbucket_importer/bitbucket_import_select_project.png)
+1. Sign in to GitLab and go to your dashboard.
+1. Click on **New project**.
-A new GitLab project will be created with your imported data.
+ ![New project in GitLab](img/bitbucket_import_new_project.png)
-### Note
-Milestones and wiki pages are not imported from Bitbucket.
+1. Click on the "Bitbucket" button
+
+ ![Bitbucket](img/import_projects_from_new_project_page.png)
+
+1. Grant GitLab access to your Bitbucket account
+
+ ![Grant access](img/bitbucket_import_grant_access.png)
+
+1. Click on the projects that you'd like to import or **Import all projects**.
+ You can also select the namespace under which each project will be
+ imported.
+
+ ![Import projects](img/bitbucket_import_select_project.png)
+
+[bb-import]: ../../integration/bitbucket.md
+[social sign-in]: ../../user/profile/account/social_sign_in.md
diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md
index c36dfdb78ec..b3660aa8030 100644
--- a/doc/workflow/importing/import_projects_from_github.md
+++ b/doc/workflow/importing/import_projects_from_github.md
@@ -40,7 +40,7 @@ namespace that started the import process.
The importer page is visible when you create a new project.
-![New project page on GitLab](img/import_projects_from_github_new_project_page.png)
+![New project page on GitLab](img/import_projects_from_new_project_page.png)
Click on the **GitHub** link and the import authorization process will start.
There are two ways to authorize access to your GitHub repositories:
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index 1a4f213a792..6a7098e79d0 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -24,6 +24,7 @@ Documentation for GitLab instance administrators is under [LFS administration do
## Requirements
* Git LFS is supported in GitLab starting with version 8.2
+* Git LFS must be enabled under project settings
* [Git LFS client](https://git-lfs.github.com) version 1.0.1 and up
## Known limitations
@@ -31,10 +32,10 @@ Documentation for GitLab instance administrators is under [LFS administration do
* Git LFS v1 original API is not supported since it was deprecated early in LFS
development
* When SSH is set as a remote, Git LFS objects still go through HTTPS
-* Any Git LFS request will ask for HTTPS credentials to be provided so good Git
+* Any Git LFS request will ask for HTTPS credentials to be provided so a good Git
credentials store is recommended
* Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have
- to add the URL to Git config manually (see #troubleshooting)
+ to add the URL to Git config manually (see [troubleshooting](#troubleshooting))
>**Note**: With 8.12 GitLab added LFS support to SSH. The Git LFS communication
still goes over HTTP, but now the SSH client passes the correct credentials
@@ -48,7 +49,7 @@ check it into your Git repository:
```bash
git clone git@gitlab.example.com:group/project.git
-git lfs install # initialize the Git LFS project project
+git lfs install # initialize the Git LFS project
git lfs track "*.iso" # select the file extensions that you want to treat as large files
```
@@ -95,7 +96,7 @@ available to the project anymore. Probably the object was removed from the serve
* Local git repository is using deprecated LFS API
-### Invalid status for <url> : 501
+### Invalid status for `<url>` : 501
Git LFS will log the failures into a log file.
To view this log file, while in project directory:
@@ -106,6 +107,9 @@ git lfs logs last
If the status `error 501` is shown, it is because:
+* Git LFS is not enabled in project settings. Check your project settings and
+ enable Git LFS.
+
* Git LFS support is not enabled on the GitLab server. Check with your GitLab
administrator why Git LFS is not enabled on the server. See
[LFS administration documentation](lfs_administration.md) for instructions
diff --git a/doc/workflow/merge_commits.png b/doc/workflow/merge_commits.png
index 8aa1587cde6..4a80811c6e3 100644
--- a/doc/workflow/merge_commits.png
+++ b/doc/workflow/merge_commits.png
Binary files differ
diff --git a/doc/workflow/merge_request.png b/doc/workflow/merge_request.png
index 6aad1d82f6e..08dfc7f2468 100644
--- a/doc/workflow/merge_request.png
+++ b/doc/workflow/merge_request.png
Binary files differ
diff --git a/doc/workflow/merge_when_build_succeeds.md b/doc/workflow/merge_when_build_succeeds.md
index 95afd12ebdb..b4f6d6117de 100644
--- a/doc/workflow/merge_when_build_succeeds.md
+++ b/doc/workflow/merge_when_build_succeeds.md
@@ -1 +1 @@
-This document was moved to [user/project/merge_requests/merge_when_build_succeeds](../user/project/merge_requests/merge_when_build_succeeds.md).
+This document was moved to [merge_when_pipeline_succeeds](../user/project/merge_requests/merge_when_pipeline_succeeds.md).
diff --git a/doc/workflow/messy_flow.png b/doc/workflow/messy_flow.png
index 8d2c0dae8c2..7e72e2a3be6 100644
--- a/doc/workflow/messy_flow.png
+++ b/doc/workflow/messy_flow.png
Binary files differ
diff --git a/doc/workflow/milestones/form.png b/doc/workflow/milestones/form.png
index 3965ca4d083..c4731d88543 100644
--- a/doc/workflow/milestones/form.png
+++ b/doc/workflow/milestones/form.png
Binary files differ
diff --git a/doc/workflow/milestones/group_form.png b/doc/workflow/milestones/group_form.png
index ff20df8081f..dccdb019703 100644
--- a/doc/workflow/milestones/group_form.png
+++ b/doc/workflow/milestones/group_form.png
Binary files differ
diff --git a/doc/workflow/mr_inline_comments.png b/doc/workflow/mr_inline_comments.png
index af7df3100d0..6a2e66a01ba 100644
--- a/doc/workflow/mr_inline_comments.png
+++ b/doc/workflow/mr_inline_comments.png
Binary files differ
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index 1b49a5c385f..c936e8833c6 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -66,6 +66,7 @@ Below is the table of events users can be notified of:
In all of the below cases, the notification will be sent to:
- Participants:
- the author and assignee of the issue/merge request
+ - the author of the pipeline
- authors of comments on the issue/merge request
- anyone mentioned by `@username` in the issue/merge request title or description
- anyone mentioned by `@username` in any of the comments on the issue/merge request
@@ -88,6 +89,8 @@ In all of the below cases, the notification will be sent to:
| Reopen merge request | |
| Merge merge request | |
| New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher |
+| Failed pipeline | The above, plus the author of the pipeline |
+| Successful pipeline | The above, plus the author of the pipeline |
In addition, if the title or description of an Issue or Merge Request is
diff --git a/doc/workflow/notifications/settings.png b/doc/workflow/notifications/settings.png
index d50757beffc..8a5494d16a8 100644
--- a/doc/workflow/notifications/settings.png
+++ b/doc/workflow/notifications/settings.png
Binary files differ
diff --git a/doc/workflow/production_branch.png b/doc/workflow/production_branch.png
index d88a3687151..648d5d5c92e 100644
--- a/doc/workflow/production_branch.png
+++ b/doc/workflow/production_branch.png
Binary files differ
diff --git a/doc/workflow/rebase.png b/doc/workflow/rebase.png
index df353311fa0..8b9bb61a5cc 100644
--- a/doc/workflow/rebase.png
+++ b/doc/workflow/rebase.png
Binary files differ
diff --git a/doc/workflow/release_branches.png b/doc/workflow/release_branches.png
index c2162248d25..5194d75a667 100644
--- a/doc/workflow/release_branches.png
+++ b/doc/workflow/release_branches.png
Binary files differ
diff --git a/doc/workflow/releases/new_tag.png b/doc/workflow/releases/new_tag.png
index 2456a8500f4..97519e5808f 100644
--- a/doc/workflow/releases/new_tag.png
+++ b/doc/workflow/releases/new_tag.png
Binary files differ
diff --git a/doc/workflow/releases/tags.png b/doc/workflow/releases/tags.png
index eeda967afd6..4c032f96125 100644
--- a/doc/workflow/releases/tags.png
+++ b/doc/workflow/releases/tags.png
Binary files differ
diff --git a/doc/workflow/remove_checkbox.png b/doc/workflow/remove_checkbox.png
index 3b0393deb0f..fb0e792b37b 100644
--- a/doc/workflow/remove_checkbox.png
+++ b/doc/workflow/remove_checkbox.png
Binary files differ
diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md
index a50ba305deb..1a8fc39bb33 100644
--- a/doc/workflow/todos.md
+++ b/doc/workflow/todos.md
@@ -24,16 +24,18 @@ you still have open.
A Todo appears in your Todos dashboard when:
-- an issue or merge request is assigned to you
+- an issue or merge request is assigned to you,
- you are `@mentioned` in an issue or merge request, be it the description of
- the issue/merge request or in a comment
+ the issue/merge request or in a comment,
+- build in the CI pipeline running for your merge request failed, but this
+ build is not allowed to fail.
>**Note:** Commenting on a commit will _not_ trigger a Todo.
### Manually creating a Todo
You can also add an issue or merge request to your Todos dashboard by clicking
-the "Add Todo" button in the issue or merge request sidebar.
+the "Add todo" button in the issue or merge request sidebar.
![Adding a Todo from the issuable sidebar](img/todos_add_todo_sidebar.png)
@@ -67,7 +69,7 @@ corresponding **Done** button, and it will disappear from your Todo list.
![A Todo in the Todos dashboard](img/todo_list_item.png)
A Todo can also be marked as done from the issue or merge request sidebar using
-the "Mark Done" button.
+the "Mark done" button.
![Mark Done from the issuable sidebar](img/todos_mark_done_sidebar.png)
diff --git a/features/abuse_report.feature b/features/abuse_report.feature
deleted file mode 100644
index 212972a762a..00000000000
--- a/features/abuse_report.feature
+++ /dev/null
@@ -1,17 +0,0 @@
-Feature: Abuse reports
- Background:
- Given I sign in as a user
- And user "Mike" exists
-
- Scenario: Report abuse
- Given I visit "Mike" user page
- And I click "Report abuse" button
- When I fill and submit abuse form
- Then I should see success message
-
- Scenario: Report abuse available only once
- Given I visit "Mike" user page
- And I click "Report abuse" button
- When I fill and submit abuse form
- And I visit "Mike" user page
- Then I should see a red "Report abuse" button
diff --git a/features/admin/abuse_report.feature b/features/admin/abuse_report.feature
deleted file mode 100644
index 7d4ec2556e5..00000000000
--- a/features/admin/abuse_report.feature
+++ /dev/null
@@ -1,8 +0,0 @@
-Feature: Admin Abuse reports
- Background:
- Given I sign in as an admin
- And abuse reports exist
-
- Scenario: Browse abuse reports
- When I visit abuse reports page
- Then I should see list of abuse reports
diff --git a/features/admin/active_tab.feature b/features/admin/active_tab.feature
deleted file mode 100644
index f5bb06dea7d..00000000000
--- a/features/admin/active_tab.feature
+++ /dev/null
@@ -1,54 +0,0 @@
-@admin
-Feature: Admin Active Tab
- Background:
- Given I sign in as an admin
-
- Scenario: On Admin Home
- Given I visit admin page
- Then the active main tab should be Overview
- And no other main tabs should be active
-
- Scenario: On Admin Projects
- Given I visit admin projects page
- Then the active main tab should be Overview
- And the active sub tab should be Projects
- And no other main tabs should be active
- And no other sub tabs should be active
-
- Scenario: On Admin Groups
- Given I visit admin groups page
- Then the active main tab should be Overview
- And the active sub tab should be Groups
- And no other main tabs should be active
- And no other sub tabs should be active
-
- Scenario: On Admin Users
- Given I visit admin users page
- Then the active main tab should be Overview
- And the active sub tab should be Users
- And no other main tabs should be active
- And no other sub tabs should be active
-
- Scenario: On Admin Logs
- Given I visit admin logs page
- Then the active main tab should be Monitoring
- And the active sub tab should be Logs
- And no other main tabs should be active
- And no other sub tabs should be active
-
- Scenario: On Admin Messages
- Given I visit admin messages page
- Then the active main tab should be Messages
- And no other main tabs should be active
-
- Scenario: On Admin Hooks
- Given I visit admin hooks page
- Then the active main tab should be Hooks
- And no other main tabs should be active
-
- Scenario: On Admin Resque
- Given I visit admin Resque page
- Then the active main tab should be Monitoring
- And the active sub tab should be Resque
- And no other main tabs should be active
- And no other sub tabs should be active
diff --git a/features/admin/hooks.feature b/features/admin/hooks.feature
deleted file mode 100644
index 5ca332d9f1c..00000000000
--- a/features/admin/hooks.feature
+++ /dev/null
@@ -1,9 +0,0 @@
-@admin
-Feature: Admin Hooks
- Background:
- Given I sign in as an admin
-
- Scenario: On Admin Hooks
- Given I visit admin hooks page
- Then I submit the form with enabled SSL verification
- And I see new hook with enabled SSL verification \ No newline at end of file
diff --git a/features/admin/logs.feature b/features/admin/logs.feature
deleted file mode 100644
index ceb3bc34927..00000000000
--- a/features/admin/logs.feature
+++ /dev/null
@@ -1,8 +0,0 @@
-@admin
-Feature: Admin Logs
- Background:
- Given I sign in as an admin
-
- Scenario: On Admin Logs
- Given I visit admin logs page
- Then I should see tabs with available logs
diff --git a/features/admin/settings.feature b/features/admin/settings.feature
deleted file mode 100644
index e38eea2cfed..00000000000
--- a/features/admin/settings.feature
+++ /dev/null
@@ -1,19 +0,0 @@
-@admin
-Feature: Admin Settings
- Background:
- Given I sign in as an admin
- And I visit admin settings page
-
- Scenario: Change application settings
- When I modify settings and save form
- Then I should see application settings saved
-
- Scenario: Change Slack Service Template settings
- When I click on "Service Templates"
- And I click on "Slack" service
- And I fill out Slack settings
- Then I check all events and submit form
- And I should see service template settings saved
- Then I click on "Slack" service
- And I should see all checkboxes checked
- And I should see Slack settings saved
diff --git a/features/admin/spam_logs.feature b/features/admin/spam_logs.feature
deleted file mode 100644
index 92a5389e3a4..00000000000
--- a/features/admin/spam_logs.feature
+++ /dev/null
@@ -1,8 +0,0 @@
-Feature: Admin spam logs
- Background:
- Given I sign in as an admin
- And spam logs exist
-
- Scenario: Browse spam logs
- When I visit spam logs page
- Then I should see list of spam logs
diff --git a/features/project/wiki.feature b/features/project/wiki.feature
index 63ce3ccb536..a04228de03b 100644
--- a/features/project/wiki.feature
+++ b/features/project/wiki.feature
@@ -49,7 +49,6 @@ Feature: Project Wiki
Scenario: View all pages
Given I have an existing wiki page
And I browse to that Wiki page
- And I click on the "Pages" button
Then I should see the existing page in the pages list
Scenario: File exists in wiki repo
@@ -72,13 +71,11 @@ Feature: Project Wiki
@javascript
Scenario: New Wiki page that has a path
Given I create a New page with paths
- And I click on the "Pages" button
Then I should see non-escaped link in the pages list
@javascript
Scenario: Edit Wiki page that has a path
Given I create a New page with paths
- And I click on the "Pages" button
And I edit the Wiki page with a path
Then I should see a non-escaped path
And I should see the Editing page
@@ -88,7 +85,6 @@ Feature: Project Wiki
@javascript
Scenario: View the page history of a Wiki page that has a path
Given I create a New page with paths
- And I click on the "Pages" button
And I view the page history of a Wiki page that has a path
Then I should see a non-escaped path
And I should see the page history
@@ -96,7 +92,6 @@ Feature: Project Wiki
@javascript
Scenario: View an old page version of a Wiki page
Given I create a New page with paths
- And I click on the "Pages" button
And I edit the Wiki page with a path
Then I should see a non-escaped path
And I should see the Editing page
diff --git a/features/snippet_search.feature b/features/snippet_search.feature
deleted file mode 100644
index 834bd3b2376..00000000000
--- a/features/snippet_search.feature
+++ /dev/null
@@ -1,20 +0,0 @@
-@dashboard
-Feature: Snippet Search
- Background:
- Given I sign in as a user
- And I have public "Personal snippet one" snippet
- And I have private "Personal snippet private" snippet
- And I have a public many lined snippet
-
- Scenario: I should see my public and private snippets
- When I search for "snippet" in snippet titles
- Then I should see "Personal snippet one" in results
- And I should see "Personal snippet private" in results
-
- Scenario: I should see three surrounding lines on either side of a matching snippet line
- When I search for "line seven" in snippet contents
- Then I should see "line four" in results
- And I should see "line seven" in results
- And I should see "line ten" in results
- And I should not see "line three" in results
- And I should not see "line eleven" in results
diff --git a/features/snippets/discover.feature b/features/snippets/discover.feature
deleted file mode 100644
index 1a7e132ea25..00000000000
--- a/features/snippets/discover.feature
+++ /dev/null
@@ -1,13 +0,0 @@
-@snippets
-Feature: Snippets Discover
- Background:
- Given I sign in as a user
- And I have public "Personal snippet one" snippet
- And I have private "Personal snippet private" snippet
- And I have internal "Personal snippet internal" snippet
-
- Scenario: I should see snippets
- Given I visit snippets page
- Then I should see "Personal snippet one" in snippets
- And I should see "Personal snippet internal" in snippets
- And I should not see "Personal snippet private" in snippets
diff --git a/features/steps/abuse_reports.rb b/features/steps/abuse_reports.rb
deleted file mode 100644
index 499accb0b08..00000000000
--- a/features/steps/abuse_reports.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-class Spinach::Features::AbuseReports < Spinach::FeatureSteps
- include SharedAuthentication
-
- step 'I visit "Mike" user page' do
- visit user_path(user_mike)
- end
-
- step 'I click "Report abuse" button' do
- click_link 'Report abuse'
- end
-
- step 'I fill and submit abuse form' do
- fill_in 'abuse_report_message', with: 'This user send spam'
- click_button 'Send report'
- end
-
- step 'I should see success message' do
- page.should have_content 'Thank you for your report'
- end
-
- step 'user "Mike" exists' do
- user_mike
- end
-
- step 'I should see a red "Report abuse" button' do
- expect(page).to have_button("Already reported for abuse")
- end
-
- def user_mike
- @user_mike ||= create(:user, name: 'Mike')
- end
-end
diff --git a/features/steps/admin/abuse_reports.rb b/features/steps/admin/abuse_reports.rb
deleted file mode 100644
index 0149416c919..00000000000
--- a/features/steps/admin/abuse_reports.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-class Spinach::Features::AdminAbuseReports < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedAdmin
-
- step 'I should see list of abuse reports' do
- page.should have_content("Abuse Reports")
- page.should have_content AbuseReport.first.message
- page.should have_link("Remove user")
- end
-
- step 'abuse reports exist' do
- create(:abuse_report)
- end
-end
diff --git a/features/steps/admin/active_tab.rb b/features/steps/admin/active_tab.rb
deleted file mode 100644
index 9b1689a8198..00000000000
--- a/features/steps/admin/active_tab.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-class Spinach::Features::AdminActiveTab < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedActiveTab
-
- step 'the active main tab should be Overview' do
- ensure_active_main_tab('Overview')
- end
-
- step 'the active sub tab should be Projects' do
- ensure_active_sub_tab('Projects')
- end
-
- step 'the active sub tab should be Groups' do
- ensure_active_sub_tab('Groups')
- end
-
- step 'the active sub tab should be Users' do
- ensure_active_sub_tab('Users')
- end
-
- step 'the active main tab should be Hooks' do
- ensure_active_main_tab('Hooks')
- end
-
- step 'the active main tab should be Monitoring' do
- ensure_active_main_tab('Monitoring')
- end
-
- step 'the active sub tab should be Resque' do
- ensure_active_sub_tab('Background Jobs')
- end
-
- step 'the active sub tab should be Logs' do
- ensure_active_sub_tab('Logs')
- end
-
- step 'the active main tab should be Messages' do
- ensure_active_main_tab('Messages')
- end
-end
diff --git a/features/steps/admin/hooks.rb b/features/steps/admin/hooks.rb
deleted file mode 100644
index 541e25fcb70..00000000000
--- a/features/steps/admin/hooks.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-class Spinach::Features::AdminHooks < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedAdmin
-
- step "I submit the form with enabled SSL verification" do
- fill_in 'hook_url', with: 'http://google.com'
- check "Enable SSL verification"
- click_on "Add System Hook"
- end
-
- step "I see new hook with enabled SSL verification" do
- expect(page).to have_content "SSL Verification: enabled"
- end
-end
diff --git a/features/steps/admin/logs.rb b/features/steps/admin/logs.rb
deleted file mode 100644
index 63881d69146..00000000000
--- a/features/steps/admin/logs.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class Spinach::Features::AdminLogs < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedAdmin
-
- step 'I should see tabs with available logs' do
- expect(page).to have_content 'test.log'
- expect(page).to have_content 'githost.log'
- expect(page).to have_content 'application.log'
- end
-end
diff --git a/features/steps/admin/settings.rb b/features/steps/admin/settings.rb
deleted file mode 100644
index 11dc7f580f0..00000000000
--- a/features/steps/admin/settings.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-class Spinach::Features::AdminSettings < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedAdmin
- include Gitlab::CurrentSettings
-
- step 'I modify settings and save form' do
- uncheck 'Gravatar enabled'
- fill_in 'Home page URL', with: 'https://about.gitlab.com/'
- fill_in 'Help page text', with: 'Example text'
- click_button 'Save'
- end
-
- step 'I should see application settings saved' do
- expect(current_application_settings.gravatar_enabled).to be_falsey
- expect(current_application_settings.home_page_url).to eq "https://about.gitlab.com/"
- expect(page).to have_content "Application settings saved successfully"
- end
-
- step 'I click on "Service Templates"' do
- click_link 'Service Templates'
- end
-
- step 'I click on "Slack" service' do
- click_link 'Slack'
- end
-
- step 'I check all events and submit form' do
- page.check('Active')
- page.check('Push')
- page.check('Tag push')
- page.check('Note')
- page.check('Issue')
- page.check('Merge request')
- page.check('Build')
- page.check('Pipeline')
- click_on 'Save'
- end
-
- step 'I fill out Slack settings' do
- fill_in 'Webhook', with: 'http://localhost'
- fill_in 'Username', with: 'test_user'
- fill_in 'service_push_channel', with: '#test_channel'
- page.check('Notify only broken builds')
- end
-
- step 'I should see service template settings saved' do
- expect(page).to have_content 'Application settings saved successfully'
- end
-
- step 'I should see all checkboxes checked' do
- page.all('input[type=checkbox]').each do |checkbox|
- expect(checkbox).to be_checked
- end
- end
-
- step 'I should see Slack settings saved' do
- expect(find_field('Webhook').value).to eq 'http://localhost'
- expect(find_field('Username').value).to eq 'test_user'
- expect(find('#service_push_channel').value).to eq '#test_channel'
- end
-end
diff --git a/features/steps/admin/spam_logs.rb b/features/steps/admin/spam_logs.rb
deleted file mode 100644
index ad825fd414c..00000000000
--- a/features/steps/admin/spam_logs.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-class Spinach::Features::AdminSpamLogs < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedAdmin
-
- step 'I should see list of spam logs' do
- expect(page).to have_content('Spam Logs')
- expect(page).to have_content spam_log.source_ip
- expect(page).to have_content spam_log.noteable_type
- expect(page).to have_content 'N'
- expect(page).to have_content spam_log.title
- expect(page).to have_content truncate(spam_log.description)
- expect(page).to have_link('Remove user')
- expect(page).to have_link('Block user')
- end
-
- step 'spam logs exist' do
- create(:spam_log)
- end
-
- def spam_log
- @spam_log ||= SpamLog.first
- end
-
- def truncate(description)
- "#{spam_log.description[0...97]}..."
- end
-end
diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb
index cefc55d07ab..adaf375453c 100644
--- a/features/steps/group/members.rb
+++ b/features/steps/group/members.rb
@@ -117,7 +117,12 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
member = mary_jane_member
page.within "#group_member_#{member.id}" do
- select 'Developer', from: "member_access_level_#{member.id}"
+ click_button member.human_access
+
+ page.within '.dropdown-menu' do
+ click_link 'Developer'
+ end
+
wait_for_ajax
end
end
diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb
index f5fddab357d..c1d1eca9116 100644
--- a/features/steps/group/milestones.rb
+++ b/features/steps/group/milestones.rb
@@ -131,5 +131,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
issue.labels << project.labels.find_by(title: 'bug')
issue.labels << project.labels.find_by(title: 'feature')
end
+
+ current_user.refresh_authorized_projects
end
end
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index b50f5238e80..aaf0ede67e6 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -62,7 +62,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'I click link "New Issue"' do
- click_link "New Issue"
+ page.has_link?('New Issue') ? click_link('New Issue') : click_link('New issue')
end
step 'I click "author" dropdown' do
diff --git a/features/steps/project/labels.rb b/features/steps/project/labels.rb
index 118ffef4774..dbeb07c78db 100644
--- a/features/steps/project/labels.rb
+++ b/features/steps/project/labels.rb
@@ -2,9 +2,7 @@ class Spinach::Features::Labels < Spinach::FeatureSteps
include SharedAuthentication
include SharedIssuable
include SharedProject
- include SharedNote
include SharedPaths
- include SharedMarkdown
step 'And I visit project "Shop" labels page' do
visit namespace_project_labels_path(project.namespace, project)
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 2ccab4334eb..d2fa8cd39af 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -413,37 +413,37 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I click link "Hide inline discussion" of the third file' do
- page.within '.files [id^=diff]:nth-child(3)' do
+ page.within '.files>div:nth-child(3)' do
find('.js-toggle-diff-comments').trigger('click')
end
end
step 'I click link "Show inline discussion" of the third file' do
- page.within '.files [id^=diff]:nth-child(3)' do
+ page.within '.files>div:nth-child(3)' do
find('.js-toggle-diff-comments').trigger('click')
end
end
step 'I should not see a comment like "Line is wrong" in the third file' do
- page.within '.files [id^=diff]:nth-child(3)' do
+ page.within '.files>div:nth-child(3)' do
expect(page).not_to have_visible_content "Line is wrong"
end
end
step 'I should see a comment like "Line is wrong" in the third file' do
- page.within '.files [id^=diff]:nth-child(3) .note-body > .note-text' do
+ page.within '.files>div:nth-child(3) .note-body > .note-text' do
expect(page).to have_visible_content "Line is wrong"
end
end
step 'I should not see a comment like "Line is wrong here" in the third file' do
- page.within '.files [id^=diff]:nth-child(3)' do
+ page.within '.files>div:nth-child(3)' do
expect(page).not_to have_visible_content "Line is wrong here"
end
end
step 'I should see a comment like "Line is wrong here" in the third file' do
- page.within '.files [id^=diff]:nth-child(3) .note-body > .note-text' do
+ page.within '.files>div:nth-child(3) .note-body > .note-text' do
expect(page).to have_visible_content "Line is wrong here"
end
end
@@ -456,7 +456,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
click_button "Comment"
end
- page.within ".files [id^=diff]:nth-child(2) .note-body > .note-text" do
+ page.within ".files>div:nth-child(2) .note-body > .note-text" do
expect(page).to have_content "Line is correct"
end
end
@@ -471,7 +471,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I should still see a comment like "Line is correct" in the second file' do
- page.within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do
+ page.within '.files>div:nth-child(2) .note-body > .note-text' do
expect(page).to have_visible_content "Line is correct"
end
end
@@ -494,7 +494,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I should see comments on the side-by-side diff page' do
- page.within '.files [id^=diff]:nth-child(2) .parallel .note-body > .note-text' do
+ page.within '.files>div:nth-child(2) .parallel .note-body > .note-text' do
expect(page).to have_visible_content "Line is correct"
end
end
@@ -515,7 +515,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see new target branch changes' do
expect(page).to have_content 'Request to merge fix into feature'
- expect(page).to have_content 'Target branch changed from merge-test to feature'
+ expect(page).to have_content 'changed target branch from merge-test to feature'
wait_for_ajax
end
diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb
index 5e7d539add6..a3bebfa4b71 100644
--- a/features/steps/project/snippets.rb
+++ b/features/steps/project/snippets.rb
@@ -22,7 +22,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
end
step 'I click link "New snippet"' do
- click_link "New snippet"
+ first(:link, "New snippet").click
end
step 'I click link "Snippet one"' do
diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb
index 2134dae168a..dee6a8a5558 100644
--- a/features/steps/project/source/markdown_render.rb
+++ b/features/steps/project/source/markdown_render.rb
@@ -241,7 +241,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
page.within(:css, ".nav-text") do
expect(page).to have_content "Test"
- expect(page).to have_content "Edit Page"
+ expect(page).to have_content "Create Page"
end
end
@@ -258,7 +258,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "api")
page.within(:css, ".nav-text") do
- expect(page).to have_content "Edit"
+ expect(page).to have_content "Create"
expect(page).to have_content "Api"
end
end
@@ -271,7 +271,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "raketasks")
page.within(:css, ".nav-text") do
- expect(page).to have_content "Edit"
+ expect(page).to have_content "Create"
expect(page).to have_content "Rake"
end
end
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index b21d0849ad1..22d971fadfb 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -65,7 +65,11 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do
- select "Reporter", from: "member_access_level_#{project_member.id}"
+ click_button project_member.human_access
+
+ page.within '.dropdown-menu' do
+ click_link 'Reporter'
+ end
end
end
@@ -144,7 +148,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
step 'I should see "Opensource" group user listing' do
page.within '.project-members-groups' do
expect(page).to have_content('OpenSource')
- expect(find('select').value).to eq('40')
+ expect(first('.group_member')).to have_content('Master')
end
end
end
diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb
index 07a955b1a14..4cb0a21fbb4 100644
--- a/features/steps/project/wiki.rb
+++ b/features/steps/project/wiki.rb
@@ -29,7 +29,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
expect(page).to have_content "link test"
click_link "link test"
- expect(page).to have_content "Edit Page"
+ expect(page).to have_content "Create Page"
end
step 'I have an existing Wiki page' do
@@ -80,13 +80,9 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
expect(page).to have_content "Page was successfully deleted"
end
- step 'I click on the "Pages" button' do
- click_on "Pages"
- end
-
step 'I should see the existing page in the pages list' do
expect(page).to have_content current_user.name
- expect(page).to have_content @page.title
+ expect(find('.wiki-pages')).to have_content @page.title.capitalize
end
step 'I have an existing Wiki page with images linked on page' do
@@ -125,7 +121,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
step 'I should see the new wiki page form' do
expect(current_path).to match('wikis/image.jpg')
expect(page).to have_content('New Wiki Page')
- expect(page).to have_content('Edit Page')
+ expect(page).to have_content('Create Page')
end
step 'I create a New page with paths' do
@@ -142,8 +138,8 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
end
step 'I edit the Wiki page with a path' do
- expect(page).to have_content('three')
- click_on 'three'
+ expect(find('.wiki-pages')).to have_content('Three')
+ click_on 'Three'
expect(find('.nav-text')).to have_content('Three')
click_on 'Edit'
end
@@ -157,7 +153,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
end
step 'I view the page history of a Wiki page that has a path' do
- click_on 'three'
+ click_on 'Three'
click_on 'Page History'
end
diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb
index 735e0ef6108..5c3e724746b 100644
--- a/features/steps/shared/authentication.rb
+++ b/features/steps/shared/authentication.rb
@@ -33,6 +33,6 @@ module SharedAuthentication
end
def current_user
- @user || User.first
+ @user || User.reorder(nil).first
end
end
diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb
index 35b71599708..11fa85ed2fe 100644
--- a/features/steps/shared/diff_note.rb
+++ b/features/steps/shared/diff_note.rb
@@ -1,6 +1,11 @@
module SharedDiffNote
include Spinach::DSL
include RepoHelpers
+ include WaitForAjax
+
+ after do
+ wait_for_ajax if javascript_test?
+ end
step 'I cancel the diff comment' do
page.within(diff_file_selector) do
diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb
index df9845ba569..79dde620265 100644
--- a/features/steps/shared/issuable.rb
+++ b/features/steps/shared/issuable.rb
@@ -110,14 +110,14 @@ module SharedIssuable
end
step 'I sort the list by "Oldest updated"' do
- find('button.dropdown-toggle.btn').click
+ find('button.dropdown-toggle').click
page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
click_link "Oldest updated"
end
end
step 'I sort the list by "Least popular"' do
- find('button.dropdown-toggle.btn').click
+ find('button.dropdown-toggle').click
page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
click_link 'Least popular'
@@ -125,7 +125,7 @@ module SharedIssuable
end
step 'I sort the list by "Most popular"' do
- find('button.dropdown-toggle.btn').click
+ find('button.dropdown-toggle').click
page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
click_link 'Most popular'
@@ -179,7 +179,7 @@ module SharedIssuable
project = Project.find_by(name: from_project_name)
expect(page).to have_content(user_name)
- expect(page).to have_content("Mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}")
+ expect(page).to have_content("mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}")
end
def expect_sidebar_content(content)
diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb
index 56b36f7c46c..a036d9b884f 100644
--- a/features/steps/shared/markdown.rb
+++ b/features/steps/shared/markdown.rb
@@ -2,7 +2,7 @@ module SharedMarkdown
include Spinach::DSL
def header_should_have_correct_id_and_link(level, text, id, parent = ".wiki")
- node = find("#{parent} h#{level} a##{id}")
+ node = find("#{parent} h#{level} a#user-content-#{id}")
expect(node[:href]).to eq "##{id}"
# Work around a weird Capybara behavior where calling `parent` on a node
diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb
index 9dc1fc41b3b..1870f6bc0c3 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -2,6 +2,10 @@ module SharedNote
include Spinach::DSL
include WaitForAjax
+ after do
+ wait_for_ajax if javascript_test?
+ end
+
step 'I delete a comment' do
page.within('.main-notes-list') do
find('.note').hover
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index 2bd8ea745e4..82c07d4f536 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -168,7 +168,7 @@ module SharedPaths
end
step 'I visit admin projects page' do
- visit admin_namespaces_projects_path
+ visit admin_projects_path
end
step 'I visit admin users page' do
@@ -203,10 +203,6 @@ module SharedPaths
visit admin_teams_path
end
- step 'I visit admin settings page' do
- visit admin_application_settings_path
- end
-
step 'I visit spam logs page' do
visit admin_spam_logs_path
end
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index cab85a48396..b51152c79c6 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -9,7 +9,7 @@ module SharedProject
step "project exists in some group namespace" do
@group = create(:group, name: 'some group')
- @project = create(:project, namespace: @group)
+ @project = create(:project, namespace: @group, public_builds: false)
end
# Create a specific project called "Shop"
diff --git a/features/steps/shared/search.rb b/features/steps/shared/search.rb
deleted file mode 100644
index 6c3d601763d..00000000000
--- a/features/steps/shared/search.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module SharedSearch
- include Spinach::DSL
-
- def search_snippet_contents(query)
- visit "/search?search=#{URI::encode(query)}&snippets=true&scope=snippet_blobs"
- end
-
- def search_snippet_titles(query)
- visit "/search?search=#{URI::encode(query)}&snippets=true&scope=snippet_titles"
- end
-end
diff --git a/features/steps/snippet_search.rb b/features/steps/snippet_search.rb
deleted file mode 100644
index 32e29ffad1e..00000000000
--- a/features/steps/snippet_search.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-class Spinach::Features::SnippetSearch < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedSnippet
- include SharedUser
- include SharedSearch
-
- step 'I search for "snippet" in snippet titles' do
- search_snippet_titles 'snippet'
- end
-
- step 'I search for "snippet private" in snippet titles' do
- search_snippet_titles 'snippet private'
- end
-
- step 'I search for "line seven" in snippet contents' do
- search_snippet_contents 'line seven'
- end
-
- step 'I should see "line seven" in results' do
- expect(page).to have_content 'line seven'
- end
-
- step 'I should see "line four" in results' do
- expect(page).to have_content 'line four'
- end
-
- step 'I should see "line ten" in results' do
- expect(page).to have_content 'line ten'
- end
-
- step 'I should not see "line eleven" in results' do
- expect(page).not_to have_content 'line eleven'
- end
-
- step 'I should not see "line three" in results' do
- expect(page).not_to have_content 'line three'
- end
-
- step 'I should see "Personal snippet one" in results' do
- expect(page).to have_content 'Personal snippet one'
- end
-
- step 'I should see "Personal snippet private" in results' do
- expect(page).to have_content 'Personal snippet private'
- end
-
- step 'I should not see "Personal snippet one" in results' do
- expect(page).not_to have_content 'Personal snippet one'
- end
-
- step 'I should not see "Personal snippet private" in results' do
- expect(page).not_to have_content 'Personal snippet private'
- end
-end
diff --git a/features/steps/snippets/discover.rb b/features/steps/snippets/discover.rb
deleted file mode 100644
index 76379d09d02..00000000000
--- a/features/steps/snippets/discover.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-class Spinach::Features::SnippetsDiscover < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedSnippet
-
- step 'I should see "Personal snippet one" in snippets' do
- expect(page).to have_content "Personal snippet one"
- end
-
- step 'I should see "Personal snippet internal" in snippets' do
- expect(page).to have_content "Personal snippet internal"
- end
-
- step 'I should not see "Personal snippet private" in snippets' do
- expect(page).not_to have_content "Personal snippet private"
- end
-
- def snippet
- @snippet ||= PersonalSnippet.find_by!(title: "Personal snippet one")
- end
-end
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
index dae0d0f918c..47372df152d 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -6,7 +6,15 @@ timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 15
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
- Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout, window_size: [1366, 768])
+ Capybara::Poltergeist::Driver.new(
+ app,
+ js_errors: true,
+ timeout: timeout,
+ window_size: [1366, 768],
+ phantomjs_options: [
+ '--load-images=no'
+ ]
+ )
end
Capybara.default_max_wait_time = timeout
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index 87915b19480..789f45489eb 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -1,5 +1,7 @@
module API
class AccessRequests < Grape::API
+ include PaginationParams
+
before { authenticate! }
helpers ::API::Helpers::MembersHelpers
@@ -13,6 +15,9 @@ module API
detail 'This feature was introduced in GitLab 8.11.'
success Entities::AccessRequester
end
+ params do
+ use :pagination
+ end
get ":id/access_requests" do
source = find_source(source_type, params[:id])
@@ -48,7 +53,7 @@ module API
put ':id/access_requests/:user_id/approve' do
source = find_source(source_type, params[:id])
- member = ::Members::ApproveAccessRequestService.new(source, current_user, declared(params)).execute
+ member = ::Members::ApproveAccessRequestService.new(source, current_user, declared_params).execute
status :created
present member.user, with: Entities::Member, member: member
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 67109ceeef9..9d5adffd8f4 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -3,6 +3,8 @@ module API
include APIGuard
version 'v3', using: :path
+ before { allow_access_with_scope :api }
+
rescue_from Gitlab::Access::AccessDeniedError do
rack_response({ 'message' => '403 Forbidden' }.to_json, 403)
end
@@ -64,6 +66,7 @@ module API
mount ::API::Session
mount ::API::Settings
mount ::API::SidekiqMetrics
+ mount ::API::Snippets
mount ::API::Subscriptions
mount ::API::SystemHooks
mount ::API::Tags
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index 8cc7a26f1fa..df6db140d0e 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -6,6 +6,9 @@ module API
module APIGuard
extend ActiveSupport::Concern
+ PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
+ PRIVATE_TOKEN_PARAM = :private_token
+
included do |base|
# OAuth2 Resource Server Authentication
use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request|
@@ -44,27 +47,60 @@ module API
access_token = find_access_token
return nil unless access_token
- case validate_access_token(access_token, scopes)
- when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
+ case AccessTokenValidationService.new(access_token).validate(scopes: scopes)
+ when AccessTokenValidationService::INSUFFICIENT_SCOPE
raise InsufficientScopeError.new(scopes)
- when Oauth2::AccessTokenValidationService::EXPIRED
+ when AccessTokenValidationService::EXPIRED
raise ExpiredError
- when Oauth2::AccessTokenValidationService::REVOKED
+ when AccessTokenValidationService::REVOKED
raise RevokedError
- when Oauth2::AccessTokenValidationService::VALID
+ when AccessTokenValidationService::VALID
@current_user = User.find(access_token.resource_owner_id)
end
end
+ def find_user_by_private_token(scopes: [])
+ token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
+
+ return nil unless token_string.present?
+
+ find_user_by_authentication_token(token_string) || find_user_by_personal_access_token(token_string, scopes)
+ end
+
def current_user
@current_user
end
+ # Set the authorization scope(s) allowed for the current request.
+ #
+ # Note: A call to this method adds to any previous scopes in place. This is done because
+ # `Grape` callbacks run from the outside-in: the top-level callback (API::API) runs first, then
+ # the next-level callback (API::API::Users, for example) runs. All these scopes are valid for the
+ # given endpoint (GET `/api/users` is accessible by the `api` and `read_user` scopes), and so they
+ # need to be stored.
+ def allow_access_with_scope(*scopes)
+ @scopes ||= []
+ @scopes.concat(scopes.map(&:to_s))
+ end
+
private
+ def find_user_by_authentication_token(token_string)
+ User.find_by_authentication_token(token_string)
+ end
+
+ def find_user_by_personal_access_token(token_string, scopes)
+ access_token = PersonalAccessToken.active.find_by_token(token_string)
+ return unless access_token
+
+ if AccessTokenValidationService.new(access_token).include_any_scope?(scopes)
+ User.find(access_token.user_id)
+ end
+ end
+
def find_access_token
@access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods)
end
@@ -72,10 +108,6 @@ module API
def doorkeeper_request
@doorkeeper_request ||= ActionDispatch::Request.new(env)
end
-
- def validate_access_token(access_token, scopes)
- Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes)
- end
end
module ClassMethods
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index e9ccba3b465..58a4df54bea 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -1,5 +1,7 @@
module API
class AwardEmoji < Grape::API
+ include PaginationParams
+
before { authenticate! }
AWARDABLES = %w[issue merge_request snippet]
@@ -21,6 +23,9 @@ module API
detail 'This feature was introduced in 8.9'
success Entities::AwardEmoji
end
+ params do
+ use :pagination
+ end
get endpoint do
if can_read_awardable?
awards = paginate(awardable.award_emoji)
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 21a106387f0..0950c3d2e88 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -23,9 +23,9 @@ module API
success Entities::RepoBranch
end
params do
- requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+ requires :branch, type: String, desc: 'The name of the branch'
end
- get ':id/repository/branches/:branch' do
+ get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do
branch = user_project.repository.find_branch(params[:branch])
not_found!("Branch") unless branch
@@ -39,11 +39,11 @@ module API
success Entities::RepoBranch
end
params do
- requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+ requires :branch, type: String, desc: 'The name of the branch'
optional :developers_can_push, type: Boolean, desc: 'Flag if developers can push to that branch'
optional :developers_can_merge, type: Boolean, desc: 'Flag if developers can merge to that branch'
end
- put ':id/repository/branches/:branch/protect' do
+ put ':id/repository/branches/:branch/protect', requirements: { branch: /.+/ } do
authorize_admin_project
branch = user_project.repository.find_branch(params[:branch])
@@ -76,9 +76,9 @@ module API
success Entities::RepoBranch
end
params do
- requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+ requires :branch, type: String, desc: 'The name of the branch'
end
- put ':id/repository/branches/:branch/unprotect' do
+ put ':id/repository/branches/:branch/unprotect', requirements: { branch: /.+/ } do
authorize_admin_project
branch = user_project.repository.find_branch(params[:branch])
@@ -112,9 +112,9 @@ module API
desc 'Delete a branch'
params do
- requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+ requires :branch, type: String, desc: 'The name of the branch'
end
- delete ":id/repository/branches/:branch" do
+ delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do
authorize_push_project
result = DeleteBranchService.new(user_project, current_user).
@@ -128,6 +128,18 @@ module API
render_api_error!(result[:message], result[:return_code])
end
end
+
+ # Delete all merged branches
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # Example Request:
+ # DELETE /projects/:id/repository/branches/delete_merged
+ delete ":id/repository/merged_branches" do
+ DeleteMergedBranchesService.new(user_project, current_user).async_execute
+
+ status(200)
+ end
end
end
end
diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb
index fb2a4148011..1217002bf8e 100644
--- a/lib/api/broadcast_messages.rb
+++ b/lib/api/broadcast_messages.rb
@@ -1,5 +1,7 @@
module API
class BroadcastMessages < Grape::API
+ include PaginationParams
+
before { authenticate! }
before { authenticated_as_admin! }
@@ -15,8 +17,7 @@ module API
success Entities::BroadcastMessage
end
params do
- optional :page, type: Integer, desc: 'Current page number'
- optional :per_page, type: Integer, desc: 'Number of messages per page'
+ use :pagination
end
get do
messages = BroadcastMessage.all
@@ -36,8 +37,7 @@ module API
optional :font, type: String, desc: 'Foreground color'
end
post do
- create_params = declared(params, include_missing: false).to_h
- message = BroadcastMessage.create(create_params)
+ message = BroadcastMessage.create(declared_params(include_missing: false))
if message.persisted?
present message, with: Entities::BroadcastMessage
@@ -73,9 +73,8 @@ module API
end
put ':id' do
message = find_message
- update_params = declared(params, include_missing: false).to_h
- if message.update(update_params)
+ if message.update(declared_params(include_missing: false))
present message, with: Entities::BroadcastMessage
else
render_validation_error!(message)
diff --git a/lib/api/builds.rb b/lib/api/builds.rb
index 67adca6605f..af61be343be 100644
--- a/lib/api/builds.rb
+++ b/lib/api/builds.rb
@@ -1,6 +1,7 @@
module API
- # Projects builds API
class Builds < Grape::API
+ include PaginationParams
+
before { authenticate! }
params do
@@ -28,6 +29,7 @@ module API
end
params do
use :optional_scope
+ use :pagination
end
get ':id/builds' do
builds = user_project.builds.order('id DESC')
@@ -41,8 +43,9 @@ module API
success Entities::Build
end
params do
- requires :sha, type: String, desc: 'The SHA id of a commit'
+ requires :sha, type: String, desc: 'The SHA id of a commit'
use :optional_scope
+ use :pagination
end
get ':id/repository/commits/:sha/builds' do
authorize_read_builds!
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index f54d4f06627..4bbdf06a49c 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -1,9 +1,10 @@
require 'mime/types'
module API
- # Project commit statuses API
class CommitStatuses < Grape::API
resource :projects do
+ include PaginationParams
+
before { authenticate! }
desc "Get a commit's statuses" do
@@ -16,6 +17,7 @@ module API
optional :stage, type: String, desc: 'The stage'
optional :name, type: String, desc: 'The name'
optional :all, type: String, desc: 'Show all statuses, default: false'
+ use :pagination
end
get ':id/repository/commits/:sha/statuses' do
authorize!(:read_commit_status, user_project)
@@ -77,7 +79,7 @@ module API
)
begin
- case params[:state].to_s
+ case params[:state]
when 'pending'
status.enqueue!
when 'running'
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 2f2cf769481..cf2489dbb67 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -1,8 +1,9 @@
require 'mime/types'
module API
- # Projects commits API
class Commits < Grape::API
+ include PaginationParams
+
before { authenticate! }
before { authorize! :download_code, user_project }
@@ -46,14 +47,14 @@ module API
requires :id, type: Integer, desc: 'The project ID'
requires :branch_name, type: String, desc: 'The name of branch'
requires :commit_message, type: String, desc: 'Commit message'
- requires :actions, type: Array, desc: 'Actions to perform in commit'
+ requires :actions, type: Array[Hash], desc: 'Actions to perform in commit'
optional :author_email, type: String, desc: 'Author email for commit'
optional :author_name, type: String, desc: 'Author name for commit'
end
post ":id/repository/commits" do
authorize! :push_code, user_project
- attrs = declared(params)
+ attrs = declared_params
attrs[:source_branch] = attrs[:branch_name]
attrs[:target_branch] = attrs[:branch_name]
attrs[:actions].map! do |action|
@@ -107,9 +108,8 @@ module API
failure [[404, 'Not Found']]
end
params do
+ use :pagination
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
- optional :per_page, type: Integer, desc: 'The amount of items per page for paginaion'
- optional :page, type: Integer, desc: 'The page number for pagination'
end
get ':id/repository/commits/:sha/comments' do
commit = user_project.commit(params[:sha])
@@ -120,6 +120,41 @@ module API
present paginate(notes), with: Entities::CommitNote
end
+ desc 'Cherry pick commit into a branch' do
+ detail 'This feature was introduced in GitLab 8.15'
+ success Entities::RepoCommit
+ end
+ params do
+ requires :sha, type: String, desc: 'A commit sha to be cherry picked'
+ requires :branch, type: String, desc: 'The name of the branch'
+ end
+ post ':id/repository/commits/:sha/cherry_pick' do
+ authorize! :push_code, user_project
+
+ commit = user_project.commit(params[:sha])
+ not_found!('Commit') unless commit
+
+ branch = user_project.repository.find_branch(params[:branch])
+ not_found!('Branch') unless branch
+
+ commit_params = {
+ commit: commit,
+ create_merge_request: false,
+ source_project: user_project,
+ source_branch: commit.cherry_pick_branch_name,
+ target_branch: params[:branch]
+ }
+
+ result = ::Commits::CherryPickService.new(user_project, current_user, commit_params).execute
+
+ if result[:status] == :success
+ branch = user_project.repository.find_branch(params[:branch])
+ present user_project.repository.commit(branch.dereferenced_target), with: Entities::RepoCommit
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+
desc 'Post comment to commit' do
success Entities::CommitNote
end
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index 425df2c176a..85360730841 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -82,7 +82,7 @@ module API
end
post ":id/#{path}/:key_id/enable" do
key = ::Projects::EnableDeployKeyService.new(user_project,
- current_user, declared(params)).execute
+ current_user, declared_params).execute
if key
present key, with: Entities::SSHKey
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
index f782bcaf7e9..c5feb49b22f 100644
--- a/lib/api/deployments.rb
+++ b/lib/api/deployments.rb
@@ -1,6 +1,8 @@
module API
# Deployments RESTfull API endpoints
class Deployments < Grape::API
+ include PaginationParams
+
before { authenticate! }
params do
@@ -12,8 +14,7 @@ module API
success Entities::Deployment
end
params do
- optional :page, type: Integer, desc: 'Page number of the current request'
- optional :per_page, type: Integer, desc: 'Number of items per page'
+ use :pagination
end
get ':id/deployments' do
authorize! :read_deployment, user_project
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 1942aeea656..dfbb3ab86dd 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -22,7 +22,7 @@ module API
expose :provider, :extern_uid
end
- class UserFull < User
+ class UserPublic < User
expose :last_sign_in_at
expose :confirmed_at
expose :email
@@ -34,7 +34,7 @@ module API
expose :external
end
- class UserLogin < UserFull
+ class UserWithPrivateToken < UserPublic
expose :private_token
end
@@ -141,8 +141,12 @@ module API
options[:project].repository.commit(repo_branch.dereferenced_target)
end
+ expose :merged do |repo_branch, options|
+ options[:project].repository.merged_to_root_ref?(repo_branch.name)
+ end
+
expose :protected do |repo_branch, options|
- options[:project].protected_branch? repo_branch.name
+ options[:project].protected_branch?(repo_branch.name)
end
expose :developers_can_push do |repo_branch, options|
@@ -159,7 +163,7 @@ module API
end
class RepoTreeObject < Grape::Entity
- expose :id, :name, :type
+ expose :id, :name, :type, :path
expose :mode do |obj, options|
filemode = obj.mode.to_s(8)
@@ -170,6 +174,7 @@ module API
class RepoCommit < Grape::Entity
expose :id, :short_id, :title, :author_name, :author_email, :created_at
+ expose :committer_name, :committer_email
expose :safe_message, as: :message
end
@@ -196,6 +201,19 @@ module API
end
end
+ class PersonalSnippet < Grape::Entity
+ expose :id, :title, :file_name
+ expose :author, using: Entities::UserBasic
+ expose :updated_at, :created_at
+
+ expose :web_url do |snippet|
+ Gitlab::UrlBuilder.build(snippet)
+ end
+ expose :raw_url do |snippet|
+ Gitlab::UrlBuilder.build(snippet) + "/raw"
+ end
+ end
+
class ProjectEntity < Grape::Entity
expose :id, :iid
expose(:project_id) { |entity| entity.project.id }
@@ -210,6 +228,7 @@ module API
class Milestone < ProjectEntity
expose :due_date
+ expose :start_date
end
class Issue < ProjectEntity
@@ -218,7 +237,7 @@ module API
expose :assignee, :author, using: Entities::UserBasic
expose :subscribed do |issue, options|
- issue.subscribed?(options[:current_user])
+ issue.subscribed?(options[:current_user], options[:project] || issue.project)
end
expose :user_notes_count
expose :upvotes, :downvotes
@@ -248,7 +267,7 @@ module API
expose :diff_head_sha, as: :sha
expose :merge_commit_sha
expose :subscribed do |merge_request, options|
- merge_request.subscribed?(options[:current_user])
+ merge_request.subscribed?(options[:current_user], options[:project])
end
expose :user_notes_count
expose :should_remove_source_branch?, as: :should_remove_source_branch
@@ -283,7 +302,7 @@ module API
end
class SSHKeyWithUser < SSHKey
- expose :user, using: Entities::UserFull
+ expose :user, using: Entities::UserPublic
end
class Note < Grape::Entity
@@ -437,13 +456,24 @@ module API
end
class Label < LabelBasic
- expose :open_issues_count, :closed_issues_count, :open_merge_requests_count
+ expose :open_issues_count do |label, options|
+ label.open_issues_count(options[:current_user])
+ end
+
+ expose :closed_issues_count do |label, options|
+ label.closed_issues_count(options[:current_user])
+ end
+
+ expose :open_merge_requests_count do |label, options|
+ label.open_merge_requests_count(options[:current_user])
+ end
+
expose :priority do |label, options|
label.priority(options[:project])
end
expose :subscribed do |label, options|
- label.subscribed?(options[:current_user])
+ label.subscribed?(options[:current_user], options[:project])
end
end
@@ -595,10 +625,11 @@ module API
expose :user, with: Entities::UserBasic
expose :created_at, :updated_at, :started_at, :finished_at, :committed_at
expose :duration
+ expose :coverage
end
class EnvironmentBasic < Grape::Entity
- expose :id, :name, :external_url
+ expose :id, :name, :slug, :external_url
end
class Environment < EnvironmentBasic
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index 819f80d8365..1a7e68f0528 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -1,6 +1,9 @@
module API
# Environments RESTfull API endpoints
class Environments < Grape::API
+ include ::API::Helpers::CustomValidators
+ include PaginationParams
+
before { authenticate! }
params do
@@ -12,8 +15,7 @@ module API
success Entities::Environment
end
params do
- optional :page, type: Integer, desc: 'Page number of the current request'
- optional :per_page, type: Integer, desc: 'Number of items per page'
+ use :pagination
end
get ':id/environments' do
authorize! :read_environment, user_project
@@ -28,12 +30,12 @@ module API
params do
requires :name, type: String, desc: 'The name of the environment to be created'
optional :external_url, type: String, desc: 'URL on which this deployment is viewable'
+ optional :slug, absence: { message: "is automatically generated and cannot be changed" }
end
post ':id/environments' do
authorize! :create_environment, user_project
- create_params = declared(params, include_parent_namespaces: false).to_h
- environment = user_project.environments.create(create_params)
+ environment = user_project.environments.create(declared_params)
if environment.persisted?
present environment, with: Entities::Environment
@@ -50,13 +52,14 @@ module API
requires :environment_id, type: Integer, desc: 'The environment ID'
optional :name, type: String, desc: 'The new environment name'
optional :external_url, type: String, desc: 'The new URL on which this deployment is viewable'
+ optional :slug, absence: { message: "is automatically generated and cannot be changed" }
end
put ':id/environments/:environment_id' do
authorize! :update_environment, user_project
environment = user_project.environments.find(params[:environment_id])
-
- update_params = declared(params, include_missing: false).extract!(:name, :external_url).to_h
+
+ update_params = declared_params(include_missing: false).extract!(:name, :external_url)
if environment.update(update_params)
present environment, with: Entities::Environment
else
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 96510e651a3..28f306e45f3 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -23,140 +23,107 @@ module API
branch_name: attrs[:branch_name]
}
end
+
+ params :simple_file_params do
+ requires :file_path, type: String, desc: 'The path to new file. Ex. lib/class.rb'
+ requires :branch_name, type: String, desc: 'The name of branch'
+ requires :commit_message, type: String, desc: 'Commit Message'
+ optional :author_email, type: String, desc: 'The email of the author'
+ optional :author_name, type: String, desc: 'The name of the author'
+ end
+
+ params :extended_file_params do
+ use :simple_file_params
+ requires :content, type: String, desc: 'File content'
+ optional :encoding, type: String, values: %w[base64], desc: 'File encoding'
+ end
end
+ params do
+ requires :id, type: String, desc: 'The project ID'
+ end
resource :projects do
- # Get file from repository
- # File content is Base64 encoded
- #
- # Parameters:
- # file_path (required) - The path to the file. Ex. lib/class.rb
- # ref (required) - The name of branch, tag or commit
- #
- # Example Request:
- # GET /projects/:id/repository/files
- #
- # Example response:
- # {
- # "file_name": "key.rb",
- # "file_path": "app/models/key.rb",
- # "size": 1476,
- # "encoding": "base64",
- # "content": "IyA9PSBTY2hlbWEgSW5mb3...",
- # "ref": "master",
- # "blob_id": "79f7bbd25901e8334750839545a9bd021f0e4c83",
- # "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50",
- # "last_commit_id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
- # }
- #
+ desc 'Get a file from repository'
+ params do
+ requires :file_path, type: String, desc: 'The path to the file. Ex. lib/class.rb'
+ requires :ref, type: String, desc: 'The name of branch, tag, or commit'
+ end
get ":id/repository/files" do
authorize! :download_code, user_project
- required_attributes! [:file_path, :ref]
- attrs = attributes_for_keys [:file_path, :ref]
- ref = attrs.delete(:ref)
- file_path = attrs.delete(:file_path)
-
- commit = user_project.commit(ref)
- not_found! 'Commit' unless commit
+ commit = user_project.commit(params[:ref])
+ not_found!('Commit') unless commit
repo = user_project.repository
- blob = repo.blob_at(commit.sha, file_path)
+ blob = repo.blob_at(commit.sha, params[:file_path])
+ not_found!('File') unless blob
- if blob
- blob.load_all_data!(repo)
- status(200)
+ blob.load_all_data!(repo)
+ status(200)
- {
- file_name: blob.name,
- file_path: blob.path,
- size: blob.size,
- encoding: "base64",
- content: Base64.strict_encode64(blob.data),
- ref: ref,
- blob_id: blob.id,
- commit_id: commit.id,
- last_commit_id: repo.last_commit_for_path(commit.sha, file_path).id
- }
- else
- not_found! 'File'
- end
+ {
+ file_name: blob.name,
+ file_path: blob.path,
+ size: blob.size,
+ encoding: "base64",
+ content: Base64.strict_encode64(blob.data),
+ ref: params[:ref],
+ blob_id: blob.id,
+ commit_id: commit.id,
+ last_commit_id: repo.last_commit_for_path(commit.sha, params[:file_path]).id
+ }
end
- # Create new file in repository
- #
- # Parameters:
- # file_path (required) - The path to new file. Ex. lib/class.rb
- # branch_name (required) - The name of branch
- # content (required) - File content
- # commit_message (required) - Commit message
- #
- # Example Request:
- # POST /projects/:id/repository/files
- #
+ desc 'Create new file in repository'
+ params do
+ use :extended_file_params
+ end
post ":id/repository/files" do
authorize! :push_code, user_project
- required_attributes! [:file_path, :branch_name, :content, :commit_message]
- attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding, :author_email, :author_name]
- result = ::Files::CreateService.new(user_project, current_user, commit_params(attrs)).execute
+ file_params = declared_params(include_missing: false)
+ result = ::Files::CreateService.new(user_project, current_user, commit_params(file_params)).execute
if result[:status] == :success
status(201)
- commit_response(attrs)
+ commit_response(file_params)
else
render_api_error!(result[:message], 400)
end
end
- # Update existing file in repository
- #
- # Parameters:
- # file_path (optional) - The path to file. Ex. lib/class.rb
- # branch_name (required) - The name of branch
- # content (required) - File content
- # commit_message (required) - Commit message
- #
- # Example Request:
- # PUT /projects/:id/repository/files
- #
+ desc 'Update existing file in repository'
+ params do
+ use :extended_file_params
+ end
put ":id/repository/files" do
authorize! :push_code, user_project
- required_attributes! [:file_path, :branch_name, :content, :commit_message]
- attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding, :author_email, :author_name]
- result = ::Files::UpdateService.new(user_project, current_user, commit_params(attrs)).execute
+ file_params = declared_params(include_missing: false)
+ result = ::Files::UpdateService.new(user_project, current_user, commit_params(file_params)).execute
if result[:status] == :success
status(200)
- commit_response(attrs)
+ commit_response(file_params)
else
http_status = result[:http_status] || 400
render_api_error!(result[:message], http_status)
end
end
- # Delete existing file in repository
- #
- # Parameters:
- # file_path (optional) - The path to file. Ex. lib/class.rb
- # branch_name (required) - The name of branch
- # content (required) - File content
- # commit_message (required) - Commit message
- #
- # Example Request:
- # DELETE /projects/:id/repository/files
- #
+ desc 'Delete an existing file in repository'
+ params do
+ use :simple_file_params
+ end
delete ":id/repository/files" do
authorize! :push_code, user_project
- required_attributes! [:file_path, :branch_name, :commit_message]
- attrs = attributes_for_keys [:file_path, :branch_name, :commit_message, :author_email, :author_name]
- result = ::Files::DeleteService.new(user_project, current_user, commit_params(attrs)).execute
+ file_params = declared_params(include_missing: false)
+ result = ::Files::DeleteService.new(user_project, current_user, commit_params(file_params)).execute
if result[:status] == :success
status(200)
- commit_response(attrs)
+ commit_response(file_params)
else
render_api_error!(result[:message], 400)
end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index a13e353b7f5..9b9d3df7435 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -1,122 +1,148 @@
module API
- # groups API
class Groups < Grape::API
+ include PaginationParams
+
before { authenticate! }
+ helpers do
+ params :optional_params do
+ optional :description, type: String, desc: 'The description of the group'
+ optional :visibility_level, type: Integer, desc: 'The visibility level of the group'
+ optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group'
+ optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
+ end
+ end
+
resource :groups do
- # Get a groups list
- #
- # Parameters:
- # skip_groups (optional) - Array of group ids to exclude from list
- # all_available (optional, boolean) - Show all group that you have access to
- # Example Request:
- # GET /groups
+ desc 'Get a groups list' do
+ success Entities::Group
+ end
+ params do
+ optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list'
+ optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
+ optional :search, type: String, desc: 'Search for a specific group'
+ optional :order_by, type: String, values: %w[name path], default: 'name', desc: 'Order by name or path'
+ optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
+ use :pagination
+ end
get do
- @groups = if current_user.admin
- Group.all
- elsif params[:all_available]
- GroupsFinder.new.execute(current_user)
- else
- current_user.groups
- end
-
- @groups = @groups.search(params[:search]) if params[:search].present?
- @groups = @groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
- @groups = paginate @groups
- present @groups, with: Entities::Group
- end
-
- # Create group. Available only for users who can create groups.
- #
- # Parameters:
- # name (required) - The name of the group
- # path (required) - The path of the group
- # description (optional) - The description of the group
- # visibility_level (optional) - The visibility level of the group
- # lfs_enabled (optional) - Enable/disable LFS for the projects in this group
- # request_access_enabled (optional) - Allow users to request member access
- # Example Request:
- # POST /groups
+ groups = if current_user.admin
+ Group.all
+ elsif params[:all_available]
+ GroupsFinder.new.execute(current_user)
+ else
+ current_user.groups
+ end
+
+ groups = groups.search(params[:search]) if params[:search].present?
+ groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
+ groups = groups.reorder(params[:order_by] => params[:sort])
+
+ present paginate(groups), with: Entities::Group
+ end
+
+ desc 'Get list of owned groups for authenticated user' do
+ success Entities::Group
+ end
+ params do
+ use :pagination
+ end
+ get '/owned' do
+ groups = current_user.owned_groups
+ present paginate(groups), with: Entities::Group, user: current_user
+ end
+
+ desc 'Create a group. Available only for users who can create groups.' do
+ success Entities::Group
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the group'
+ requires :path, type: String, desc: 'The path of the group'
+ use :optional_params
+ end
post do
authorize! :create_group
- required_attributes! [:name, :path]
- attrs = attributes_for_keys [:name, :path, :description, :visibility_level, :lfs_enabled, :request_access_enabled]
- @group = Group.new(attrs)
+ group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute
- if @group.save
- @group.add_owner(current_user)
- present @group, with: Entities::Group
+ if group.persisted?
+ present group, with: Entities::Group
else
- render_api_error!("Failed to save group #{@group.errors.messages}", 400)
+ render_api_error!("Failed to save group #{group.errors.messages}", 400)
end
end
+ end
- # Update group. Available only for users who can administrate groups.
- #
- # Parameters:
- # id (required) - The ID of a group
- # path (optional) - The path of the group
- # description (optional) - The description of the group
- # visibility_level (optional) - The visibility level of the group
- # lfs_enabled (optional) - Enable/disable LFS for the projects in this group
- # request_access_enabled (optional) - Allow users to request member access
- # Example Request:
- # PUT /groups/:id
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
+ resource :groups do
+ desc 'Update a group. Available only for users who can administrate groups.' do
+ success Entities::Group
+ end
+ params do
+ optional :name, type: String, desc: 'The name of the group'
+ optional :path, type: String, desc: 'The path of the group'
+ use :optional_params
+ at_least_one_of :name, :path, :description, :visibility_level,
+ :lfs_enabled, :request_access_enabled
+ end
put ':id' do
- group = find_group(params[:id])
+ group = find_group!(params[:id])
authorize! :admin_group, group
- attrs = attributes_for_keys [:name, :path, :description, :visibility_level, :lfs_enabled, :request_access_enabled]
-
- if ::Groups::UpdateService.new(group, current_user, attrs).execute
+ if ::Groups::UpdateService.new(group, current_user, declared_params(include_missing: false)).execute
present group, with: Entities::GroupDetail
else
render_validation_error!(group)
end
end
- # Get a single group, with containing projects
- #
- # Parameters:
- # id (required) - The ID of a group
- # Example Request:
- # GET /groups/:id
+ desc 'Get a single group, with containing projects.' do
+ success Entities::GroupDetail
+ end
get ":id" do
- group = find_group(params[:id])
+ group = find_group!(params[:id])
present group, with: Entities::GroupDetail
end
- # Remove group
- #
- # Parameters:
- # id (required) - The ID of a group
- # Example Request:
- # DELETE /groups/:id
+ desc 'Remove a group.'
delete ":id" do
- group = find_group(params[:id])
+ group = find_group!(params[:id])
authorize! :admin_group, group
DestroyGroupService.new(group, current_user).execute
end
- # Get a list of projects in this group
- #
- # Example Request:
- # GET /groups/:id/projects
+ desc 'Get a list of projects in this group.' do
+ success Entities::Project
+ end
+ 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'
+ 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'
+ optional :simple, type: Boolean, default: false,
+ desc: 'Return only the ID, URL, name, and path of each project'
+ use :pagination
+ end
get ":id/projects" do
- group = find_group(params[:id])
+ group = find_group!(params[:id])
projects = GroupProjectsFinder.new(group).execute(current_user)
- projects = paginate projects
- present projects, with: Entities::Project, user: current_user
+ projects = filter_projects(projects)
+ entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project
+ present paginate(projects), with: entity, user: current_user
end
- # Transfer a project to the Group namespace
- #
- # Parameters:
- # id - group id
- # project_id - project id
- # Example Request:
- # POST /groups/:id/projects/:project_id
+ desc 'Transfer a project to the group namespace. Available only for admin.' do
+ success Entities::GroupDetail
+ end
+ params do
+ requires :project_id, type: String, desc: 'The ID of the project'
+ end
post ":id/projects/:project_id" do
authenticated_as_admin!
group = Group.find_by(id: params[:id])
@@ -124,7 +150,7 @@ module API
result = ::Projects::TransferService.new(project, current_user).execute(group)
if result
- present group
+ present group, with: Entities::GroupDetail
else
render_api_error!("Failed to transfer project #{project.errors.messages}", 400)
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 3c9d7b1aaef..4be659fc20b 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -2,112 +2,72 @@ module API
module Helpers
include Gitlab::Utils
- PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
- PRIVATE_TOKEN_PARAM = :private_token
SUDO_HEADER = "HTTP_SUDO"
SUDO_PARAM = :sudo
- def private_token
- params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
- end
-
- def warden
- env['warden']
- end
-
- # Check the Rails session for valid authentication details
- #
- # Until CSRF protection is added to the API, disallow this method for
- # state-changing endpoints
- def find_user_from_warden
- warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD'])
- end
-
- def find_user_by_private_token
- token = private_token
- return nil unless token.present?
-
- User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
+ def declared_params(options = {})
+ options = { include_parent_namespaces: false }.merge(options)
+ declared(params, options).to_h.symbolize_keys
end
def current_user
- @current_user ||= find_user_by_private_token
- @current_user ||= doorkeeper_guard
- @current_user ||= find_user_from_warden
+ return @current_user if defined?(@current_user)
- unless @current_user && Gitlab::UserAccess.new(@current_user).allowed?
- return nil
- end
+ @current_user = initial_current_user
- identifier = sudo_identifier()
-
- # If the sudo is the current user do nothing
- if identifier && !(@current_user.id == identifier || @current_user.username == identifier)
- forbidden!('Must be admin to use sudo') unless @current_user.is_admin?
- @current_user = User.by_username_or_id(identifier)
- not_found!("No user id or username for: #{identifier}") if @current_user.nil?
- end
+ sudo!
@current_user
end
- def sudo_identifier
- identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
-
- # Regex for integers
- if !!(identifier =~ /\A[0-9]+\z/)
- identifier.to_i
- else
- identifier
- end
+ def sudo?
+ initial_current_user != current_user
end
def user_project
- @project ||= find_project(params[:id])
+ @project ||= find_project!(params[:id])
end
def available_labels
@available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute
end
- def find_project(id)
- project = Project.find_with_namespace(id) || Project.find_by(id: id)
-
- if can?(current_user, :read_project, project)
- project
+ def find_user(id)
+ if id =~ /^\d+$/
+ User.find_by(id: id)
else
- not_found!('Project')
+ User.find_by(username: id)
end
end
- def project_service
- @project_service ||= begin
- underscored_service = params[:service_slug].underscore
-
- if Service.available_services_names.include?(underscored_service)
- user_project.build_missing_services
-
- service_method = "#{underscored_service}_service"
-
- send_service(service_method)
- end
+ def find_project(id)
+ if id =~ /^\d+$/
+ Project.find_by(id: id)
+ else
+ Project.find_with_namespace(id)
end
-
- @project_service || not_found!("Service")
end
- def send_service(service_method)
- user_project.send(service_method)
- end
+ def find_project!(id)
+ project = find_project(id)
- def service_attributes
- @service_attributes ||= project_service.fields.inject([]) do |arr, hash|
- arr << hash[:name].to_sym
+ if can?(current_user, :read_project, project)
+ project
+ else
+ not_found!('Project')
end
end
def find_group(id)
- group = Group.find_by(path: id) || Group.find_by(id: id)
+ if id =~ /^\d+$/
+ Group.find_by(id: id)
+ else
+ Group.find_by_full_path(id)
+ end
+ end
+
+ def find_group!(id)
+ group = find_group(id)
if can?(current_user, :read_group, group)
group
@@ -122,9 +82,7 @@ module API
end
def find_project_issue(id)
- issue = user_project.issues.find(id)
- not_found! unless can?(current_user, :read_issue, issue)
- issue
+ IssuesFinder.new(current_user, project_id: user_project.id).find(id)
end
def paginate(relation)
@@ -137,6 +95,10 @@ module API
unauthorized! unless current_user
end
+ def authenticate_non_get!
+ authenticate! unless %w[GET HEAD].include?(route.route_method)
+ end
+
def authenticate_by_gitlab_shell_token!
input = params['secret_token'].try(:chomp)
unless Devise.secure_compare(secret_token, input)
@@ -145,6 +107,7 @@ module API
end
def authenticated_as_admin!
+ authenticate!
forbidden! unless current_user.is_admin?
end
@@ -192,20 +155,6 @@ module API
ActionController::Parameters.new(attrs).permit!
end
- # Helper method for validating all labels against its names
- def validate_label_params(params)
- errors = {}
-
- params[:labels].to_s.split(',').each do |label_name|
- label = available_labels.find_or_initialize_by(title: label_name.strip)
- next if label.valid?
-
- errors[label.title] = label.errors
- end
-
- errors
- end
-
# Checks the occurrences of datetime attributes, each attribute if present in the params hash must be in ISO 8601
# format (YYYY-MM-DDTHH:MM:SSZ) or a Bad Request error is invoked.
#
@@ -222,22 +171,6 @@ module API
end
end
- def issuable_order_by
- if params["order_by"] == 'updated_at'
- 'updated_at'
- else
- 'created_at'
- end
- end
-
- def issuable_sort
- if params["sort"] == 'asc'
- :asc
- else
- :desc
- end
- end
-
def filter_by_iid(items, iid)
items.where(iid: iid)
end
@@ -318,11 +251,6 @@ module API
# Projects helpers
def filter_projects(projects)
- # If the archived parameter is passed, limit results accordingly
- if params[:archived].present?
- projects = projects.where(archived: to_boolean(params[:archived]))
- end
-
if params[:search].present?
projects = projects.search(params[:search])
end
@@ -331,25 +259,8 @@ module API
projects = projects.search_by_visibility(params[:visibility])
end
- projects.reorder(project_order_by => project_sort)
- end
-
- def project_order_by
- order_fields = %w(id name path created_at updated_at last_activity_at)
-
- if order_fields.include?(params['order_by'])
- params['order_by']
- else
- 'created_at'
- end
- end
-
- def project_sort
- if params["sort"] == 'asc'
- :asc
- else
- :desc
- end
+ projects = projects.where(archived: params[:archived])
+ projects.reorder(params[:order_by] => params[:sort])
end
# file helpers
@@ -394,6 +305,62 @@ module API
private
+ def private_token
+ params[APIGuard::PRIVATE_TOKEN_PARAM] || env[APIGuard::PRIVATE_TOKEN_HEADER]
+ end
+
+ def warden
+ env['warden']
+ end
+
+ # Check the Rails session for valid authentication details
+ #
+ # Until CSRF protection is added to the API, disallow this method for
+ # state-changing endpoints
+ def find_user_from_warden
+ warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD'])
+ end
+
+ def initial_current_user
+ return @initial_current_user if defined?(@initial_current_user)
+
+ @initial_current_user ||= find_user_by_private_token(scopes: @scopes)
+ @initial_current_user ||= doorkeeper_guard(scopes: @scopes)
+ @initial_current_user ||= find_user_from_warden
+
+ unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed?
+ @initial_current_user = nil
+ end
+
+ @initial_current_user
+ end
+
+ def sudo!
+ return unless sudo_identifier
+ return unless initial_current_user
+
+ unless initial_current_user.is_admin?
+ forbidden!('Must be admin to use sudo')
+ end
+
+ # Only private tokens should be used for the SUDO feature
+ unless private_token == initial_current_user.private_token
+ forbidden!('Private token must be specified in order to use sudo')
+ end
+
+ sudoed_user = find_user(sudo_identifier)
+
+ if sudoed_user
+ @current_user = sudoed_user
+ else
+ not_found!("No user id or username for: #{sudo_identifier}")
+ end
+ end
+
+ def sudo_identifier
+ @sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
+ end
+
def add_pagination_headers(paginated_data)
header 'X-Total', paginated_data.total_count.to_s
header 'X-Total-Pages', paginated_data.total_pages.to_s
diff --git a/lib/api/helpers/custom_validators.rb b/lib/api/helpers/custom_validators.rb
new file mode 100644
index 00000000000..0a8f3073a50
--- /dev/null
+++ b/lib/api/helpers/custom_validators.rb
@@ -0,0 +1,14 @@
+module API
+ module Helpers
+ module CustomValidators
+ class Absence < Grape::Validations::Base
+ def validate_param!(attr_name, params)
+ return if params.respond_to?(:key?) && !params.key?(attr_name)
+ raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:absence)
+ end
+ end
+ end
+ end
+end
+
+Grape::Validations.register_validator(:absence, ::API::Helpers::CustomValidators::Absence)
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
new file mode 100644
index 00000000000..eb223c1101d
--- /dev/null
+++ b/lib/api/helpers/internal_helpers.rb
@@ -0,0 +1,57 @@
+module API
+ module Helpers
+ module InternalHelpers
+ # Project paths may be any of the following:
+ # * /repository/storage/path/namespace/project
+ # * /namespace/project
+ # * namespace/project
+ #
+ # In addition, they may have a '.git' extension and multiple namespaces
+ #
+ # Transform all these cases to 'namespace/project'
+ def clean_project_path(project_path, storage_paths = Repository.storages.values)
+ project_path = project_path.sub(/\.git\z/, '')
+
+ storage_paths.each do |storage_path|
+ storage_path = File.expand_path(storage_path)
+
+ if project_path.start_with?(storage_path)
+ project_path = project_path.sub(storage_path, '')
+ break
+ end
+ end
+
+ project_path.sub(/\A\//, '')
+ end
+
+ def project_path
+ @project_path ||= clean_project_path(params[:project])
+ end
+
+ def wiki?
+ @wiki ||= project_path.end_with?('.wiki') &&
+ !Project.find_with_namespace(project_path)
+ end
+
+ def project
+ @project ||= begin
+ # Check for *.wiki repositories.
+ # Strip out the .wiki from the pathname before finding the
+ # project. This applies the correct project permissions to
+ # the wiki repository as well.
+ project_path.chomp!('.wiki') if wiki?
+
+ Project.find_with_namespace(project_path)
+ end
+ end
+
+ def ssh_authentication_abilities
+ [
+ :read_project,
+ :download_code,
+ :push_code
+ ]
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb
index 90114f6f667..d9cae1501f8 100644
--- a/lib/api/helpers/members_helpers.rb
+++ b/lib/api/helpers/members_helpers.rb
@@ -2,7 +2,7 @@ module API
module Helpers
module MembersHelpers
def find_source(source_type, id)
- public_send("find_#{source_type}", id)
+ public_send("find_#{source_type}!", id)
end
def authorize_admin_source!(source_type, source)
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index ccf181402f9..7087ce11401 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -3,6 +3,8 @@ module API
class Internal < Grape::API
before { authenticate_by_gitlab_shell_token! }
+ helpers ::API::Helpers::InternalHelpers
+
namespace 'internal' do
# Check if git command is allowed to project
#
@@ -14,42 +16,6 @@ module API
# ref - branch name
# forced_push - forced_push
# protocol - Git access protocol being used, e.g. HTTP or SSH
- #
-
- helpers do
- def project_path
- @project_path ||= begin
- project_path = params[:project].sub(/\.git\z/, '')
- Repository.remove_storage_from_path(project_path)
- end
- end
-
- def wiki?
- @wiki ||= project_path.end_with?('.wiki') &&
- !Project.find_with_namespace(project_path)
- end
-
- def project
- @project ||= begin
- # Check for *.wiki repositories.
- # Strip out the .wiki from the pathname before finding the
- # project. This applies the correct project permissions to
- # the wiki repository as well.
- project_path.chomp!('.wiki') if wiki?
-
- Project.find_with_namespace(project_path)
- end
- end
-
- def ssh_authentication_abilities
- [
- :read_project,
- :download_code,
- :push_code
- ]
- end
- end
-
post "/allowed" do
status 200
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index c9689e6f8ef..c9124649cbb 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -1,6 +1,7 @@
module API
- # Issues API
class Issues < Grape::API
+ include PaginationParams
+
before { authenticate! }
helpers do
@@ -19,98 +20,89 @@ module API
def filter_issues_milestone(issues, milestone)
issues.includes(:milestone).where('milestones.title' => milestone)
end
+
+ params :issues_params do
+ optional :labels, type: String, desc: 'Comma-separated list of label names'
+ 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.'
+ 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'
+ optional :state_event, type: String, values: %w[open close],
+ desc: 'State of the issue'
+ end
end
resource :issues do
- # Get currently authenticated user's issues
- #
- # Parameters:
- # state (optional) - Return "opened" or "closed" issues
- # labels (optional) - Comma-separated list of label names
- # order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`
- # sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
- #
- # Example Requests:
- # GET /issues
- # GET /issues?state=opened
- # GET /issues?state=closed
- # GET /issues?labels=foo
- # GET /issues?labels=foo,bar
- # GET /issues?labels=foo,bar&state=opened
+ desc "Get currently authenticated user's issues" do
+ success Entities::Issue
+ end
+ params do
+ optional :state, type: String, values: %w[opened closed all], default: 'all',
+ desc: 'Return opened, closed, or all issues'
+ use :issues_params
+ end
get do
issues = current_user.issues.inc_notes_with_associations
- issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
+ issues = filter_issues_state(issues, params[:state])
issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
- issues = issues.reorder(issuable_order_by => issuable_sort)
+ issues = issues.reorder(params[:order_by] => params[:sort])
present paginate(issues), with: Entities::Issue, current_user: current_user
end
end
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
resource :groups do
- # Get a list of group issues
- #
- # Parameters:
- # id (required) - The ID of a group
- # state (optional) - Return "opened" or "closed" issues
- # labels (optional) - Comma-separated list of label names
- # milestone (optional) - Milestone title
- # order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`
- # sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
- #
- # Example Requests:
- # GET /groups/:id/issues
- # GET /groups/:id/issues?state=opened
- # GET /groups/:id/issues?state=closed
- # GET /groups/:id/issues?labels=foo
- # GET /groups/:id/issues?labels=foo,bar
- # GET /groups/:id/issues?labels=foo,bar&state=opened
- # GET /groups/:id/issues?milestone=1.0.0
- # GET /groups/:id/issues?milestone=1.0.0&state=closed
+ desc 'Get a list of group issues' do
+ success 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])
+ group = find_group!(params.delete(:id))
- params[:state] ||= 'opened'
params[:group_id] = group.id
params[:milestone_title] = params.delete(:milestone)
params[:label_name] = params.delete(:labels)
- if params[:order_by] || params[:sort]
- # The Sortable concern takes 'created_desc', not 'created_at_desc' (for example)
- params[:sort] = "#{issuable_order_by.sub('_at', '')}_#{issuable_sort}"
- end
-
issues = IssuesFinder.new(current_user, params).execute
+ issues = issues.reorder(params[:order_by] => params[:sort])
present paginate(issues), with: Entities::Issue, current_user: current_user
end
end
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
- # Get a list of project issues
- #
- # Parameters:
- # id (required) - The ID of a project
- # iid (optional) - Return the project issue having the given `iid`
- # state (optional) - Return "opened" or "closed" issues
- # labels (optional) - Comma-separated list of label names
- # milestone (optional) - Milestone title
- # order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`
- # sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
- #
- # Example Requests:
- # GET /projects/:id/issues
- # GET /projects/:id/issues?state=opened
- # GET /projects/:id/issues?state=closed
- # GET /projects/:id/issues?labels=foo
- # 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=closed
- # GET /issues?iid=42
+ desc 'Get a list of project issues' do
+ success Entities::Issue
+ end
+ params do
+ optional :state, type: String, values: %w[opened closed all], default: 'all',
+ desc: 'Return opened, closed, or all issues'
+ optional :iid, type: Integer, desc: 'The IID of the issue'
+ use :issues_params
+ end
get ":id/issues" do
- issues = user_project.issues.inc_notes_with_associations.visible_to_user(current_user)
- issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
+ issues = IssuesFinder.new(current_user, project_id: user_project.id).execute.inc_notes_with_associations
+ issues = filter_issues_state(issues, params[:state])
issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil?
@@ -118,143 +110,121 @@ module API
issues = filter_issues_milestone(issues, params[:milestone])
end
- issues = issues.reorder(issuable_order_by => issuable_sort)
-
- present paginate(issues), with: Entities::Issue, current_user: current_user
+ issues = issues.reorder(params[:order_by] => params[:sort])
+ present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project
end
- # Get a single project issue
- #
- # Parameters:
- # id (required) - The ID of a project
- # issue_id (required) - The ID of a project issue
- # Example Request:
- # GET /projects/:id/issues/:issue_id
+ desc 'Get a single project issue' do
+ success 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: Entities::Issue, current_user: current_user
+ issue = find_project_issue(params[:issue_id])
+ present issue, with: Entities::Issue, current_user: current_user, project: user_project
end
- # Create a new project issue
- #
- # Parameters:
- # id (required) - The ID of a project
- # title (required) - The title of an issue
- # description (optional) - The description of an issue
- # assignee_id (optional) - The ID of a user to assign issue
- # milestone_id (optional) - The ID of a milestone to assign issue
- # labels (optional) - The labels of an issue
- # created_at (optional) - Date time string, ISO 8601 formatted
- # due_date (optional) - Date time string in the format YEAR-MONTH-DAY
- # confidential (optional) - Boolean parameter if the issue should be confidential
- # Example Request:
- # POST /projects/:id/issues
+ desc 'Create a new project issue' do
+ success 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
- required_attributes! [:title]
-
- keys = [:title, :description, :assignee_id, :milestone_id, :due_date, :confidential]
- keys << :created_at if current_user.admin? || user_project.owner == current_user
- attrs = attributes_for_keys(keys)
-
- # Validate label names in advance
- if (errors = validate_label_params(params)).any?
- render_api_error!({ labels: errors }, 400)
+ # 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
- attrs[:labels] = params[:labels] if params[:labels]
+ issue_params = declared_params(include_missing: false)
- # Convert and filter out invalid confidential flags
- attrs['confidential'] = to_boolean(attrs['confidential'])
- attrs.delete('confidential') if attrs['confidential'].nil?
-
- issue = ::Issues::CreateService.new(user_project, current_user, attrs.merge(request: request, api: true)).execute
+ 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: Entities::Issue, current_user: current_user
+ present issue, with: Entities::Issue, current_user: current_user, project: user_project
else
render_validation_error!(issue)
end
end
- # Update an existing issue
- #
- # Parameters:
- # id (required) - The ID of a project
- # issue_id (required) - The ID of a project issue
- # title (optional) - The title of an issue
- # description (optional) - The description of an issue
- # assignee_id (optional) - The ID of a user to assign issue
- # milestone_id (optional) - The ID of a milestone to assign issue
- # labels (optional) - The labels of an issue
- # state_event (optional) - The state event of an issue (close|reopen)
- # updated_at (optional) - Date time string, ISO 8601 formatted
- # due_date (optional) - Date time string in the format YEAR-MONTH-DAY
- # confidential (optional) - Boolean parameter if the issue should be confidential
- # Example Request:
- # PUT /projects/:id/issues/:issue_id
+ desc 'Update an existing issue' do
+ success 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.'
+ 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[:issue_id])
+ issue = user_project.issues.find(params.delete(:issue_id))
authorize! :update_issue, issue
- keys = [:title, :description, :assignee_id, :milestone_id, :state_event, :due_date, :confidential]
- keys << :updated_at if current_user.admin? || user_project.owner == current_user
- attrs = attributes_for_keys(keys)
- # Validate label names in advance
- if (errors = validate_label_params(params)).any?
- render_api_error!({ labels: errors }, 400)
+ # 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
- attrs[:labels] = params[:labels] if params[:labels]
-
- # Convert and filter out invalid confidential flags
- attrs['confidential'] = to_boolean(attrs['confidential'])
- attrs.delete('confidential') if attrs['confidential'].nil?
-
- issue = ::Issues::UpdateService.new(user_project, current_user, attrs).execute(issue)
+ issue = ::Issues::UpdateService.new(user_project,
+ current_user,
+ declared_params(include_missing: false)).execute(issue)
if issue.valid?
- present issue, with: Entities::Issue, current_user: current_user
+ present issue, with: Entities::Issue, current_user: current_user, project: user_project
else
render_validation_error!(issue)
end
end
- # Move an existing issue
- #
- # Parameters:
- # id (required) - The ID of a project
- # issue_id (required) - The ID of a project issue
- # to_project_id (required) - The ID of the new project
- # Example Request:
- # POST /projects/:id/issues/:issue_id/move
+ desc 'Move an existing issue' do
+ success 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
- required_attributes! [:to_project_id]
+ issue = user_project.issues.find_by(id: params[:issue_id])
+ not_found!('Issue') unless issue
- issue = user_project.issues.find(params[:issue_id])
- new_project = Project.find(params[:to_project_id])
+ 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: Entities::Issue, current_user: current_user
+ present issue, with: Entities::Issue, current_user: current_user, project: user_project
rescue ::Issues::MoveService::MoveError => error
render_api_error!(error.message, 400)
end
end
- #
- # Delete a project issue
- #
- # Parameters:
- # id (required) - The ID of a project
- # issue_id (required) - The ID of a project issue
- # Example Request:
- # DELETE /projects/:id/issues/:issue_id
+ 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
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index 97218054f37..652786d4e3e 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -30,10 +30,7 @@ module API
conflict!('Label already exists') if label
priority = params.delete(:priority)
- label_params = declared(params,
- include_parent_namespaces: false,
- include_missing: false).to_h
- label = user_project.labels.create(label_params)
+ label = user_project.labels.create(declared_params(include_missing: false))
if label.valid?
label.prioritize!(user_project, priority) if priority
@@ -77,11 +74,9 @@ module API
update_priority = params.key?(:priority)
priority = params.delete(:priority)
- label_params = declared(params,
- include_parent_namespaces: false,
- include_missing: false).to_h
+ label_params = declared_params(include_missing: false)
# Rename new name to the actual label attribute name
- label_params[:name] = label_params.delete('new_name') if label_params.key?('new_name')
+ label_params[:name] = label_params.delete(:new_name) if label_params.key?(:new_name)
render_validation_error!(label) unless label.update(label_params)
diff --git a/lib/api/members.rb b/lib/api/members.rb
index b80818f0eb6..d85f1f78cd6 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -1,5 +1,7 @@
module API
class Members < Grape::API
+ include PaginationParams
+
before { authenticate! }
helpers ::API::Helpers::MembersHelpers
@@ -14,15 +16,15 @@ module API
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]
- users = paginate(users)
- present users, with: Entities::Member, source: source
+ present paginate(users), with: Entities::Member, source: source
end
desc 'Gets a member of a group or project.' do
@@ -120,7 +122,7 @@ module API
if member.nil?
{ message: "Access revoked", id: params[:user_id].to_i }
else
- ::Members::DestroyService.new(source, current_user, declared(params)).execute
+ ::Members::DestroyService.new(source, current_user, declared_params).execute
present member.user, with: Entities::Member, member: member
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index bf8504e1101..5d1fe22f2df 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -1,8 +1,14 @@
module API
- # MergeRequest 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
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
helpers do
def handle_merge_request_errors!(errors)
@@ -18,90 +24,77 @@ module API
render_api_error!(errors, 400)
end
+
+ params :optional_params do
+ optional :description, type: String, desc: 'The description of the merge request'
+ optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
+ optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
+ optional :labels, type: String, desc: 'Comma-separated list of label names'
+ optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
+ end
end
- # List merge requests
- #
- # Parameters:
- # id (required) - The ID of a project
- # iid (optional) - Return the project MR having the given `iid`
- # state (optional) - Return requests "merged", "opened" or "closed"
- # order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`
- # sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
- #
- # Example:
- # GET /projects/:id/merge_requests
- # GET /projects/:id/merge_requests?state=opened
- # GET /projects/:id/merge_requests?state=closed
- # GET /projects/:id/merge_requests?order_by=created_at
- # GET /projects/:id/merge_requests?order_by=updated_at
- # GET /projects/:id/merge_requests?sort=desc
- # GET /projects/:id/merge_requests?sort=asc
- # GET /projects/:id/merge_requests?iid=42
- #
+ desc 'List merge requests' do
+ success Entities::MergeRequest
+ end
+ params do
+ optional :state, type: String, values: %w[opened closed merged all], default: 'all',
+ desc: 'Return opened, closed, merged, or all merge requests'
+ optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
+ desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return merge requests sorted in `asc` or `desc` order.'
+ optional :iid, type: 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
- unless params[:iid].nil?
- merge_requests = filter_by_iid(merge_requests, params[:iid])
- end
+ 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
+ case params[:state]
+ when 'opened' then merge_requests.opened
+ when 'closed' then merge_requests.closed
+ when 'merged' then merge_requests.merged
else merge_requests
end
- merge_requests = merge_requests.reorder(issuable_order_by => issuable_sort)
- present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user
+ merge_requests = merge_requests.reorder(params[:order_by] => params[:sort])
+ present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user, project: user_project
end
- # Create MR
- #
- # Parameters:
- #
- # id (required) - The ID of a project - this will be the source of the merge request
- # source_branch (required) - The source branch
- # target_branch (required) - The target branch
- # target_project_id - The target project of the merge request defaults to the :id of the project
- # assignee_id - Assignee user ID
- # title (required) - Title of MR
- # description - Description of MR
- # labels (optional) - Labels for MR as a comma-separated list
- # milestone_id (optional) - Milestone ID
- #
- # Example:
- # POST /projects/:id/merge_requests
- #
+ desc 'Create a merge request' do
+ success Entities::MergeRequest
+ end
+ params do
+ requires :title, type: String, desc: 'The title of the merge request'
+ requires :source_branch, type: String, desc: 'The source branch'
+ requires :target_branch, type: String, desc: 'The target branch'
+ optional :target_project_id, type: Integer,
+ desc: 'The target project of the merge request defaults to the :id of the project'
+ use :optional_params
+ end
post ":id/merge_requests" do
authorize! :create_merge_request, user_project
- required_attributes! [:source_branch, :target_branch, :title]
- attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id, :description, :milestone_id]
- # Validate label names in advance
- if (errors = validate_label_params(params)).any?
- render_api_error!({ labels: errors }, 400)
- end
-
- attrs[:labels] = params[:labels] if params[:labels]
+ 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, attrs).execute
+ merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute
if merge_request.valid?
- present merge_request, with: Entities::MergeRequest, current_user: current_user
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
else
handle_merge_request_errors! merge_request.errors
end
end
- # Delete a MR
- #
- # Parameters:
- # id (required) - The ID of the project
- # merge_request_id (required) - The MR id
+ desc 'Delete a merge request'
+ params do
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ end
delete ":id/merge_requests/:merge_request_id" do
merge_request = user_project.merge_requests.find_by(id: params[:merge_request_id])
@@ -112,109 +105,80 @@ module API
# Routing "merge_request/:merge_request_id/..." is DEPRECATED and WILL BE REMOVED in version 9.0
# Use "merge_requests/:merge_request_id/..." instead.
#
- [":id/merge_request/:merge_request_id", ":id/merge_requests/:merge_request_id"].each do |path|
- # Show MR
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - The ID of MR
- #
- # Example:
- # GET /projects/:id/merge_requests/:merge_request_id
- #
+ params do
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ end
+ { ":id/merge_request/:merge_request_id" => :deprecated, ":id/merge_requests/:merge_request_id" => :ok }.each do |path, status|
+ desc 'Get a single merge request' do
+ if status == :deprecated
+ detail DEPRECATION_MESSAGE
+ end
+ success Entities::MergeRequest
+ end
get path do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
-
authorize! :read_merge_request, merge_request
-
- present merge_request, with: Entities::MergeRequest, current_user: current_user
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end
- # Show MR commits
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - The ID of MR
- #
- # Example:
- # GET /projects/:id/merge_requests/:merge_request_id/commits
- #
+ desc 'Get the commits of a merge request' do
+ success Entities::RepoCommit
+ end
get "#{path}/commits" do
- merge_request = user_project.merge_requests.
- find(params[:merge_request_id])
+ merge_request = user_project.merge_requests.find(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request.commits, with: Entities::RepoCommit
end
- # Show MR changes
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - The ID of MR
- #
- # Example:
- # GET /projects/:id/merge_requests/:merge_request_id/changes
- #
+ desc 'Show the merge request changes' do
+ success Entities::MergeRequestChanges
+ end
get "#{path}/changes" do
- merge_request = user_project.merge_requests.
- find(params[:merge_request_id])
+ merge_request = user_project.merge_requests.find(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
end
- # Update MR
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- # target_branch - The target branch
- # assignee_id - Assignee user ID
- # title - Title of MR
- # state_event - Status of MR. (close|reopen|merge)
- # description - Description of MR
- # labels (optional) - Labels for a MR as a comma-separated list
- # milestone_id (optional) - Milestone ID
- # Example:
- # PUT /projects/:id/merge_requests/:merge_request_id
- #
+ desc 'Update a merge request' do
+ success Entities::MergeRequest
+ end
+ params do
+ optional :title, type: String, 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
- attrs = attributes_for_keys [:target_branch, :assignee_id, :title, :state_event, :description, :milestone_id]
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = user_project.merge_requests.find(params.delete(:merge_request_id))
authorize! :update_merge_request, merge_request
- # Ensure source_branch is not specified
- if params[:source_branch].present?
- render_api_error!('Source branch cannot be changed', 400)
- end
-
- # Validate label names in advance
- if (errors = validate_label_params(params)).any?
- render_api_error!({ labels: errors }, 400)
- end
+ 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?
- attrs[:labels] = params[:labels] if params[:labels]
-
- merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, attrs).execute(merge_request)
+ merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
if merge_request.valid?
- present merge_request, with: Entities::MergeRequest, current_user: current_user
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
else
handle_merge_request_errors! merge_request.errors
end
end
- # Merge MR
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- # merge_commit_message (optional) - Custom merge commit message
- # should_remove_source_branch (optional) - When true, the source branch will be deleted if possible
- # merge_when_build_succeeds (optional) - When true, this MR will be merged when the build succeeds
- # sha (optional) - When present, must have the HEAD SHA of the source branch
- # Example:
- # PUT /projects/:id/merge_requests/:merge_request_id/merge
- #
+ desc 'Merge a merge request' do
+ success Entities::MergeRequest
+ end
+ params do
+ optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
+ optional :should_remove_source_branch, type: Boolean,
+ desc: 'When true, the source branch will be deleted if possible'
+ optional :merge_when_build_succeeds, type: Boolean,
+ desc: 'When true, this merge request will be merged when the 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 = user_project.merge_requests.find(params[:merge_request_id])
@@ -235,41 +199,39 @@ module API
should_remove_source_branch: params[:should_remove_source_branch]
}
- if to_boolean(params[:merge_when_build_succeeds]) && merge_request.pipeline && merge_request.pipeline.active?
- ::MergeRequests::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params).
- execute(merge_request)
+ 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)
+ ::MergeRequests::MergeService
+ .new(merge_request.target_project, current_user, merge_params)
+ .execute(merge_request)
end
- present merge_request, with: Entities::MergeRequest, current_user: current_user
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end
- # Cancel Merge if Merge When build succeeds is enabled
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- #
+ desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do
+ success Entities::MergeRequest
+ end
post "#{path}/cancel_merge_when_build_succeeds" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
- ::MergeRequest::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user).cancel(merge_request)
+ ::MergeRequest::MergeWhenPipelineSucceedsService
+ .new(merge_request.target_project, current_user)
+ .cancel(merge_request)
end
- # Duplicate. DEPRECATED and WILL BE REMOVED in 9.0.
- # Use GET "/projects/:id/merge_requests/:merge_request_id/notes" instead
- #
- # Get a merge request's comments
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- # Examples:
- # GET /projects/:id/merge_requests/:merge_request_id/comments
- #
+ desc 'Get the comments of a merge request' do
+ detail 'Duplicate. DEPRECATED and WILL BE REMOVED in 9.0'
+ success Entities::MRNote
+ end
+ params do
+ use :pagination
+ end
get "#{path}/comments" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
@@ -278,23 +240,15 @@ module API
present paginate(merge_request.notes.fresh), with: Entities::MRNote
end
- # Duplicate. DEPRECATED and WILL BE REMOVED in 9.0.
- # Use POST "/projects/:id/merge_requests/:merge_request_id/notes" instead
- #
- # Post comment to merge request
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- # note (required) - Text of comment
- # Examples:
- # POST /projects/:id/merge_requests/:merge_request_id/comments
- #
+ desc 'Post a comment to a merge request' do
+ detail 'Duplicate. DEPRECATED and WILL BE REMOVED in 9.0'
+ success Entities::MRNote
+ end
+ params do
+ requires :note, type: String, desc: 'The text of the comment'
+ end
post "#{path}/comments" do
- required_attributes! [:note]
-
merge_request = user_project.merge_requests.find(params[:merge_request_id])
-
authorize! :create_note, merge_request
opts = {
@@ -312,13 +266,12 @@ module API
end
end
- # List issues that will close on merge
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- # Examples:
- # GET /projects/:id/merge_requests/:merge_request_id/closes_issues
+ desc 'List issues that will be closed on merge' do
+ success Entities::MRNote
+ end
+ params do
+ use :pagination
+ end
get "#{path}/closes_issues" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb
index 9b73f6826cf..3c373a84ec5 100644
--- a/lib/api/milestones.rb
+++ b/lib/api/milestones.rb
@@ -1,6 +1,7 @@
module API
- # Milestones API
class Milestones < Grape::API
+ include PaginationParams
+
before { authenticate! }
helpers do
@@ -11,19 +12,27 @@ module API
else milestones
end
end
+
+ params :optional_params do
+ optional :description, type: String, desc: 'The description of the milestone'
+ optional :due_date, type: String, desc: 'The due date of the milestone. The ISO 8601 date format (%Y-%m-%d)'
+ optional :start_date, type: String, desc: 'The start date of the milestone. The ISO 8601 date format (%Y-%m-%d)'
+ end
end
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
- # Get a list of project milestones
- #
- # Parameters:
- # id (required) - The ID of a project
- # state (optional) - Return "active" or "closed" milestones
- # Example Request:
- # GET /projects/:id/milestones
- # GET /projects/:id/milestones?iid=42
- # GET /projects/:id/milestones?state=active
- # GET /projects/:id/milestones?state=closed
+ desc 'Get a list of project milestones' do
+ success Entities::Milestone
+ end
+ params do
+ optional :state, type: String, values: %w[active closed all], default: 'all',
+ desc: 'Return "active", "closed", or "all" milestones'
+ optional :iid, type: Array[Integer], desc: 'The IID of the milestone'
+ use :pagination
+ end
get ":id/milestones" do
authorize! :read_milestone, user_project
@@ -34,34 +43,30 @@ module API
present paginate(milestones), with: Entities::Milestone
end
- # Get a single project milestone
- #
- # Parameters:
- # id (required) - The ID of a project
- # milestone_id (required) - The ID of a project milestone
- # Example Request:
- # GET /projects/:id/milestones/:milestone_id
+ desc 'Get a single project milestone' do
+ success Entities::Milestone
+ end
+ params do
+ requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+ end
get ":id/milestones/:milestone_id" do
authorize! :read_milestone, user_project
- @milestone = user_project.milestones.find(params[:milestone_id])
- present @milestone, with: Entities::Milestone
+ milestone = user_project.milestones.find(params[:milestone_id])
+ present milestone, with: Entities::Milestone
end
- # Create a new project milestone
- #
- # Parameters:
- # id (required) - The ID of the project
- # title (required) - The title of the milestone
- # description (optional) - The description of the milestone
- # due_date (optional) - The due date of the milestone
- # Example Request:
- # POST /projects/:id/milestones
+ desc 'Create a new project milestone' do
+ success Entities::Milestone
+ end
+ params do
+ requires :title, type: String, desc: 'The title of the milestone'
+ use :optional_params
+ end
post ":id/milestones" do
authorize! :admin_milestone, user_project
- required_attributes! [:title]
- attrs = attributes_for_keys [:title, :description, :due_date]
- milestone = ::Milestones::CreateService.new(user_project, current_user, attrs).execute
+
+ milestone = ::Milestones::CreateService.new(user_project, current_user, declared_params).execute
if milestone.valid?
present milestone, with: Entities::Milestone
@@ -70,22 +75,23 @@ module API
end
end
- # Update an existing project milestone
- #
- # Parameters:
- # id (required) - The ID of a project
- # milestone_id (required) - The ID of a project milestone
- # title (optional) - The title of a milestone
- # description (optional) - The description of a milestone
- # due_date (optional) - The due date of a milestone
- # state_event (optional) - The state event of the milestone (close|activate)
- # Example Request:
- # PUT /projects/:id/milestones/:milestone_id
+ desc 'Update an existing project milestone' do
+ success Entities::Milestone
+ end
+ params do
+ requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+ optional :title, type: String, desc: 'The title of the milestone'
+ optional :state_event, type: String, values: %w[close activate],
+ desc: 'The state event of the milestone '
+ use :optional_params
+ at_least_one_of :title, :description, :due_date, :state_event
+ end
put ":id/milestones/:milestone_id" do
authorize! :admin_milestone, user_project
- attrs = attributes_for_keys [:title, :description, :due_date, :state_event]
- milestone = user_project.milestones.find(params[:milestone_id])
- milestone = ::Milestones::UpdateService.new(user_project, current_user, attrs).execute(milestone)
+ milestone = user_project.milestones.find(params.delete(:milestone_id))
+
+ milestone_params = declared_params(include_missing: false)
+ milestone = ::Milestones::UpdateService.new(user_project, current_user, milestone_params).execute(milestone)
if milestone.valid?
present milestone, with: Entities::Milestone
@@ -94,25 +100,25 @@ module API
end
end
- # Get all issues for a single project milestone
- #
- # Parameters:
- # id (required) - The ID of a project
- # milestone_id (required) - The ID of a project milestone
- # Example Request:
- # GET /projects/:id/milestones/:milestone_id/issues
+ desc 'Get all issues for a single project milestone' do
+ success Entities::Issue
+ end
+ params do
+ requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+ use :pagination
+ end
get ":id/milestones/:milestone_id/issues" do
authorize! :read_milestone, user_project
- @milestone = user_project.milestones.find(params[:milestone_id])
+ milestone = user_project.milestones.find(params[:milestone_id])
finder_params = {
project_id: user_project.id,
- milestone_title: @milestone.title
+ milestone_title: milestone.title
}
issues = IssuesFinder.new(current_user, finder_params).execute
- present paginate(issues), with: Entities::Issue, current_user: current_user
+ present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project
end
end
end
diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb
index fe981d7b9fa..30761cb9b55 100644
--- a/lib/api/namespaces.rb
+++ b/lib/api/namespaces.rb
@@ -1,6 +1,7 @@
module API
- # namespaces API
class Namespaces < Grape::API
+ include PaginationParams
+
before { authenticate! }
resource :namespaces do
@@ -9,6 +10,7 @@ module API
end
params do
optional :search, type: String, desc: "Search query for namespaces"
+ use :pagination
end
get do
namespaces = current_user.admin ? Namespace.all : current_user.namespaces
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index c5c214d4d13..d0faf17714b 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -1,27 +1,29 @@
module API
- # Notes API
class Notes < Grape::API
+ include PaginationParams
+
before { authenticate! }
NOTEABLE_TYPES = [Issue, MergeRequest, Snippet]
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
NOTEABLE_TYPES.each do |noteable_type|
noteables_str = noteable_type.to_s.underscore.pluralize
- noteable_id_str = "#{noteable_type.to_s.underscore}_id"
-
- # Get a list of project +noteable+ notes
- #
- # Parameters:
- # id (required) - The ID of a project
- # noteable_id (required) - The ID of an issue or snippet
- # Example Request:
- # GET /projects/:id/issues/:noteable_id/notes
- # GET /projects/:id/snippets/:noteable_id/notes
- get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do
- @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym])
-
- if can?(current_user, noteable_read_ability_name(@noteable), @noteable)
+
+ desc 'Get a list of project +noteable+ notes' do
+ success Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ use :pagination
+ end
+ get ":id/#{noteables_str}/:noteable_id/notes" do
+ noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+
+ if can?(current_user, noteable_read_ability_name(noteable), noteable)
# We exclude notes that are cross-references and that cannot be viewed
# by the current user. By doing this exclusion at this level and not
# at the DB query level (which we cannot in that case), the current
@@ -31,7 +33,7 @@ module API
# paginate() only works with a relation. This could lead to a
# mismatch between the pagination headers info and the actual notes
# array returned, but this is really a edge-case.
- paginate(@noteable.notes).
+ paginate(noteable.notes).
reject { |n| n.cross_reference_not_visible_for?(current_user) }
present notes, with: Entities::Note
else
@@ -39,44 +41,40 @@ module API
end
end
- # Get a single +noteable+ note
- #
- # Parameters:
- # id (required) - The ID of a project
- # noteable_id (required) - The ID of an issue or snippet
- # note_id (required) - The ID of a note
- # Example Request:
- # GET /projects/:id/issues/:noteable_id/notes/:note_id
- # GET /projects/:id/snippets/:noteable_id/notes/:note_id
- get ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do
- @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym])
- @note = @noteable.notes.find(params[:note_id])
- can_read_note = can?(current_user, noteable_read_ability_name(@noteable), @noteable) && !@note.cross_reference_not_visible_for?(current_user)
+ desc 'Get a single +noteable+ note' do
+ success Entities::Note
+ end
+ params do
+ requires :note_id, type: Integer, desc: 'The ID of a note'
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ end
+ get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
+ noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+ note = noteable.notes.find(params[:note_id])
+ can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user)
if can_read_note
- present @note, with: Entities::Note
+ present note, with: Entities::Note
else
not_found!("Note")
end
end
- # Create a new +noteable+ note
- #
- # Parameters:
- # id (required) - The ID of a project
- # noteable_id (required) - The ID of an issue or snippet
- # body (required) - The content of a note
- # created_at (optional) - The date
- # Example Request:
- # POST /projects/:id/issues/:noteable_id/notes
- # POST /projects/:id/snippets/:noteable_id/notes
- post ":id/#{noteables_str}/:#{noteable_id_str}/notes" do
+ desc 'Create a new +noteable+ note' do
+ success Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :body, type: String, desc: 'The content of a note'
+ optional :created_at, type: String, desc: 'The creation date of the note'
+ end
+ post ":id/#{noteables_str}/:noteable_id/notes" do
required_attributes! [:body]
opts = {
note: params[:body],
noteable_type: noteables_str.classify,
- noteable_id: params[noteable_id_str]
+ noteable_id: params[:noteable_id]
}
if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user)
@@ -92,19 +90,15 @@ module API
end
end
- # Modify existing +noteable+ note
- #
- # Parameters:
- # id (required) - The ID of a project
- # noteable_id (required) - The ID of an issue or snippet
- # node_id (required) - The ID of a note
- # body (required) - New content of a note
- # Example Request:
- # PUT /projects/:id/issues/:noteable_id/notes/:note_id
- # PUT /projects/:id/snippets/:noteable_id/notes/:node_id
- put ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do
- required_attributes! [:body]
-
+ desc 'Update an existing +noteable+ note' do
+ success Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :note_id, type: Integer, desc: 'The ID of a note'
+ requires :body, type: String, desc: 'The content of a note'
+ end
+ put ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
note = user_project.notes.find(params[:note_id])
authorize! :admin_note, note
@@ -113,25 +107,23 @@ module API
note: params[:body]
}
- @note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note)
+ note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note)
- if @note.valid?
- present @note, with: Entities::Note
+ if note.valid?
+ present note, with: Entities::Note
else
render_api_error!("Failed to save note #{note.errors.messages}", 400)
end
end
- # Delete a +noteable+ note
- #
- # Parameters:
- # id (required) - The ID of a project
- # noteable_id (required) - The ID of an issue, MR, or snippet
- # node_id (required) - The ID of a note
- # Example Request:
- # DELETE /projects/:id/issues/:noteable_id/notes/:note_id
- # DELETE /projects/:id/snippets/:noteable_id/notes/:node_id
- delete ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do
+ desc 'Delete a +noteable+ note' do
+ success Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :note_id, type: Integer, desc: 'The ID of a note'
+ end
+ delete ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
note = user_project.notes.find(params[:note_id])
authorize! :admin_note, note
diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb
index a70a7e71073..c5e9b3ad69b 100644
--- a/lib/api/notification_settings.rb
+++ b/lib/api/notification_settings.rb
@@ -33,10 +33,9 @@ module API
begin
notification_setting.transaction do
new_notification_email = params.delete(:notification_email)
- declared_params = declared(params, include_missing: false).to_h
current_user.update(notification_email: new_notification_email) if new_notification_email
- notification_setting.update(declared_params)
+ notification_setting.update(declared_params(include_missing: false))
end
rescue ArgumentError => e # catch level enum error
render_api_error! e.to_s, 400
@@ -81,9 +80,7 @@ module API
notification_setting = current_user.notification_settings_for(source)
begin
- declared_params = declared(params, include_missing: false).to_h
-
- notification_setting.update(declared_params)
+ notification_setting.update(declared_params(include_missing: false))
rescue ArgumentError => e # catch level enum error
render_api_error! e.to_s, 400
end
diff --git a/lib/api/pagination_params.rb b/lib/api/pagination_params.rb
new file mode 100644
index 00000000000..8c1e4381a74
--- /dev/null
+++ b/lib/api/pagination_params.rb
@@ -0,0 +1,24 @@
+module API
+ # Concern for declare pagination params.
+ #
+ # @example
+ # class CustomApiResource < Grape::API
+ # include PaginationParams
+ #
+ # params do
+ # use :pagination
+ # end
+ # end
+ module PaginationParams
+ extend ActiveSupport::Concern
+
+ included do
+ helpers do
+ params :pagination do
+ optional :page, type: Integer, desc: 'Current page number'
+ optional :per_page, type: Integer, desc: 'Number of items per page'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index 2a0c8e1f2c0..b634b1d0222 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -1,5 +1,7 @@
module API
class Pipelines < Grape::API
+ include PaginationParams
+
before { authenticate! }
params do
@@ -11,8 +13,7 @@ module API
success Entities::Pipeline
end
params do
- optional :page, type: Integer, desc: 'Page number of the current request'
- optional :per_page, type: Integer, desc: 'Number of items per page'
+ use :pagination
optional :scope, type: String, values: ['running', 'branches', 'tags'],
desc: 'Either running, branches, or tags'
end
@@ -22,6 +23,27 @@ module API
pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope])
present paginate(pipelines), with: Entities::Pipeline
end
+
+ desc 'Create a new pipeline' do
+ detail 'This feature was introduced in GitLab 8.14'
+ success Entities::Pipeline
+ end
+ params do
+ requires :ref, type: String, desc: 'Reference'
+ end
+ post ':id/pipeline' do
+ authorize! :create_pipeline, user_project
+
+ new_pipeline = Ci::CreatePipelineService.new(user_project,
+ current_user,
+ declared_params(include_missing: false))
+ .execute(ignore_skip_ci: true, save_on_errors: false)
+ if new_pipeline.persisted?
+ present new_pipeline, with: Entities::Pipeline
+ else
+ render_validation_error!(new_pipeline)
+ end
+ end
desc 'Gets a specific pipeline for the project' do
detail 'This feature was introduced in GitLab 8.11'
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index eef343c2ac6..dcc0fb7a911 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -1,6 +1,10 @@
module API
- # Projects API
class ProjectHooks < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+ before { authorize_admin_project }
+
helpers do
params :project_hook_properties do
requires :url, type: String, desc: "The URL to send the request to"
@@ -17,9 +21,6 @@ module API
end
end
- before { authenticate! }
- before { authorize_admin_project }
-
params do
requires :id, type: String, desc: 'The ID of a project'
end
@@ -27,6 +28,9 @@ module API
desc 'Get project hooks' do
success Entities::ProjectHook
end
+ params do
+ use :pagination
+ end
get ":id/hooks" do
hooks = paginate user_project.hooks
@@ -51,8 +55,7 @@ module API
use :project_hook_properties
end
post ":id/hooks" do
- new_hook_params = declared(params, include_missing: false, include_parent_namespaces: false).to_h
- hook = user_project.hooks.new(new_hook_params)
+ hook = user_project.hooks.new(declared_params(include_missing: false))
if hook.save
present hook, with: Entities::ProjectHook
@@ -71,12 +74,9 @@ module API
use :project_hook_properties
end
put ":id/hooks/:hook_id" do
- hook = user_project.hooks.find(params[:hook_id])
-
- new_params = declared(params, include_missing: false, include_parent_namespaces: false).to_h
- new_params.delete('hook_id')
+ hook = user_project.hooks.find(params.delete(:hook_id))
- if hook.update_attributes(new_params)
+ if hook.update_attributes(declared_params(include_missing: false))
present hook, with: Entities::ProjectHook
else
error!("Invalid url given", 422) if hook.errors[:url].present?
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index ce1bf0d26d2..9d8c5b63685 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -1,8 +1,12 @@
module API
- # Projects API
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)
@@ -18,111 +22,111 @@ module API
end
end
- # Get a project snippets
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # GET /projects/:id/snippets
+ desc 'Get all project snippets' do
+ success Entities::ProjectSnippet
+ end
+ params do
+ use :pagination
+ end
get ":id/snippets" do
present paginate(snippets_for_current_user), with: Entities::ProjectSnippet
end
- # Get a project snippet
- #
- # Parameters:
- # id (required) - The ID of a project
- # snippet_id (required) - The ID of a project snippet
- # Example Request:
- # GET /projects/:id/snippets/:snippet_id
+ desc 'Get a single project snippet' do
+ success 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: Entities::ProjectSnippet
- end
-
- # Create a new project snippet
- #
- # Parameters:
- # id (required) - The ID of a project
- # title (required) - The title of a snippet
- # file_name (required) - The name of a snippet file
- # code (required) - The content of a snippet
- # visibility_level (required) - The snippet's visibility
- # Example Request:
- # POST /projects/:id/snippets
+ snippet = snippets_for_current_user.find(params[:snippet_id])
+ present snippet, with: Entities::ProjectSnippet
+ end
+
+ desc 'Create a new project snippet' do
+ success 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
- required_attributes! [:title, :file_name, :code, :visibility_level]
+ snippet_params = declared_params
+ snippet_params[:content] = snippet_params.delete(:code)
- attrs = attributes_for_keys [:title, :file_name, :visibility_level]
- attrs[:content] = params[:code] if params[:code].present?
- @snippet = CreateSnippetService.new(user_project, current_user,
- attrs).execute
+ snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute
- if @snippet.errors.any?
- render_validation_error!(@snippet)
+ if snippet.persisted?
+ present snippet, with: Entities::ProjectSnippet
else
- present @snippet, with: Entities::ProjectSnippet
+ render_validation_error!(snippet)
end
end
- # Update an existing project snippet
- #
- # Parameters:
- # id (required) - The ID of a project
- # snippet_id (required) - The ID of a project snippet
- # title (optional) - The title of a snippet
- # file_name (optional) - The name of a snippet file
- # code (optional) - The content of a snippet
- # visibility_level (optional) - The snippet's visibility
- # Example Request:
- # PUT /projects/:id/snippets/:snippet_id
+ desc 'Update an existing project snippet' do
+ success 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(params[:snippet_id])
- authorize! :update_project_snippet, @snippet
+ 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?
- attrs = attributes_for_keys [:title, :file_name, :visibility_level]
- attrs[:content] = params[:code] if params[:code].present?
+ UpdateSnippetService.new(user_project, current_user, snippet,
+ snippet_params).execute
- UpdateSnippetService.new(user_project, current_user, @snippet,
- attrs).execute
- if @snippet.errors.any?
- render_validation_error!(@snippet)
+ if snippet.persisted?
+ present snippet, with: Entities::ProjectSnippet
else
- present @snippet, with: Entities::ProjectSnippet
+ render_validation_error!(snippet)
end
end
- # Delete a project snippet
- #
- # Parameters:
- # id (required) - The ID of a project
- # snippet_id (required) - The ID of a project snippet
- # Example Request:
- # DELETE /projects/:id/snippets/:snippet_id
+ 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
- begin
- @snippet = snippets_for_current_user.find(params[:snippet_id])
- authorize! :update_project_snippet, @snippet
- @snippet.destroy
- rescue
- not_found!('Snippet')
- end
+ snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
+ not_found!('Snippet') unless snippet
+
+ authorize! :admin_project_snippet, snippet
+ snippet.destroy
end
- # Get a raw project snippet
- #
- # Parameters:
- # id (required) - The ID of a project
- # snippet_id (required) - The ID of a project snippet
- # Example Request:
- # GET /projects/:id/snippets/:snippet_id/raw
+ 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(params[:snippet_id])
+ 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
+ present snippet.content
end
end
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 6b856128c2e..2929d2157dc 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -1,293 +1,295 @@
module API
# Projects API
class Projects < Grape::API
- before { authenticate! }
+ 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
- resource :projects, requirements: { id: /[^\/]+/ } do
- helpers do
- def map_public_to_visibility_level(attrs)
- publik = attrs.delete(:public)
- if publik.present? && !attrs[:visibility_level].present?
- publik = to_boolean(publik)
- # 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
+ 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
- # Get a projects list for authenticated user
- #
- # Example Request:
- # GET /projects
- get do
- projects = current_user.authorized_projects
- projects = filter_projects(projects)
- projects = paginate projects
- entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
+ resource :projects do
+ helpers do
+ 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'
+ use :sort_params
+ end
- present projects, with: entity, user: current_user
+ 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
end
- # Get a list of visible projects for authenticated user
- #
- # Example Request:
- # GET /projects/visible
+ desc 'Get a list of visible projects for authenticated user' do
+ success Entities::BasicProjectDetails
+ end
+ params do
+ optional :simple, type: Boolean, default: false,
+ desc: 'Return only the ID, URL, name, and path of each project'
+ use :filter_params
+ use :pagination
+ end
get '/visible' do
projects = ProjectsFinder.new.execute(current_user)
projects = filter_projects(projects)
- projects = paginate projects
+ entity = params[:simple] || !current_user ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
+
+ present paginate(projects), with: entity, user: current_user
+ end
+
+ desc 'Get a projects list for authenticated user' do
+ success Entities::BasicProjectDetails
+ end
+ params do
+ optional :simple, type: Boolean, default: false,
+ desc: 'Return only the ID, URL, name, and path of each project'
+ use :filter_params
+ use :pagination
+ end
+ get do
+ authenticate!
+
+ projects = current_user.authorized_projects
+ projects = filter_projects(projects)
entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
- present projects, with: entity, user: current_user
+ present paginate(projects), with: entity, user: current_user
end
- # Get an owned projects list for authenticated user
- #
- # Example Request:
- # GET /projects/owned
+ desc 'Get an owned projects list for authenticated user' do
+ success Entities::BasicProjectDetails
+ end
+ params do
+ use :filter_params
+ use :pagination
+ end
get '/owned' do
+ authenticate!
+
projects = current_user.owned_projects
projects = filter_projects(projects)
- projects = paginate projects
- present projects, with: Entities::ProjectWithAccess, user: current_user
+
+ present paginate(projects), with: Entities::ProjectWithAccess, user: current_user
end
- # Gets starred project for the authenticated user
- #
- # Example Request:
- # GET /projects/starred
+ desc 'Gets starred project for the authenticated user' do
+ success Entities::BasicProjectDetails
+ end
+ params do
+ use :filter_params
+ use :pagination
+ end
get '/starred' do
+ authenticate!
+
projects = current_user.viewable_starred_projects
projects = filter_projects(projects)
- projects = paginate projects
- present projects, with: Entities::Project, user: current_user
+
+ present paginate(projects), with: Entities::Project, user: current_user
end
- # Get all projects for admin user
- #
- # Example Request:
- # GET /projects/all
+ desc 'Get all projects for admin user' do
+ success Entities::BasicProjectDetails
+ end
+ params do
+ use :filter_params
+ use :pagination
+ end
get '/all' do
authenticated_as_admin!
+
projects = Project.all
projects = filter_projects(projects)
- projects = paginate projects
- present projects, with: Entities::ProjectWithAccess, user: current_user
+
+ present paginate(projects), with: Entities::ProjectWithAccess, user: current_user
end
- # Get a single project
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # GET /projects/:id
- get ":id" do
- present user_project, with: Entities::ProjectWithAccess, user: current_user,
- user_can_admin_project: can?(current_user, :admin_project, user_project)
+ 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" 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])
- # Get events for a single project
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # GET /projects/:id/events
- get ":id/events" do
- events = paginate user_project.events.recent
- present events, with: Entities::Event
- end
-
- # Create new project
- #
- # Parameters:
- # name (required) - name for new project
- # description (optional) - short project description
- # issues_enabled (optional)
- # merge_requests_enabled (optional)
- # builds_enabled (optional)
- # wiki_enabled (optional)
- # snippets_enabled (optional)
- # container_registry_enabled (optional)
- # shared_runners_enabled (optional)
- # namespace_id (optional) - defaults to user namespace
- # public (optional) - if true same as setting visibility_level = 20
- # visibility_level (optional) - 0 by default
- # import_url (optional)
- # public_builds (optional)
- # lfs_enabled (optional)
- # request_access_enabled (optional) - Allow users to request member access
- # Example Request
- # POST /projects
+ present paginate(projects), with: Entities::Project
+ end
+
+ desc 'Create new project' do
+ success 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
- required_attributes! [:name]
- attrs = attributes_for_keys [:builds_enabled,
- :container_registry_enabled,
- :description,
- :import_url,
- :issues_enabled,
- :lfs_enabled,
- :merge_requests_enabled,
- :name,
- :namespace_id,
- :only_allow_merge_if_build_succeeds,
- :path,
- :public,
- :public_builds,
- :request_access_enabled,
- :shared_runners_enabled,
- :snippets_enabled,
- :visibility_level,
- :wiki_enabled,
- :only_allow_merge_if_all_discussions_are_resolved]
- attrs = map_public_to_visibility_level(attrs)
- @project = ::Projects::CreateService.new(current_user, attrs).execute
- if @project.saved?
- present @project, with: Entities::Project,
- user_can_admin_project: can?(current_user, :admin_project, @project)
+ 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: 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)
+ if project.errors[:limit_reached].present?
+ error!(project.errors[:limit_reached], 403)
end
- render_validation_error!(@project)
+ render_validation_error!(project)
end
end
- # Create new project for a specified user. Only available to admin users.
- #
- # Parameters:
- # user_id (required) - The ID of a user
- # name (required) - name for new project
- # description (optional) - short project description
- # default_branch (optional) - 'master' by default
- # issues_enabled (optional)
- # merge_requests_enabled (optional)
- # builds_enabled (optional)
- # wiki_enabled (optional)
- # snippets_enabled (optional)
- # container_registry_enabled (optional)
- # shared_runners_enabled (optional)
- # public (optional) - if true same as setting visibility_level = 20
- # visibility_level (optional)
- # import_url (optional)
- # public_builds (optional)
- # lfs_enabled (optional)
- # request_access_enabled (optional) - Allow users to request member access
- # Example Request
- # POST /projects/user/:user_id
+ desc 'Create new project for a specified user. Only available to admin users.' do
+ success 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(params[:user_id])
- attrs = attributes_for_keys [:builds_enabled,
- :default_branch,
- :description,
- :import_url,
- :issues_enabled,
- :lfs_enabled,
- :merge_requests_enabled,
- :name,
- :only_allow_merge_if_build_succeeds,
- :public,
- :public_builds,
- :request_access_enabled,
- :shared_runners_enabled,
- :snippets_enabled,
- :visibility_level,
- :wiki_enabled,
- :only_allow_merge_if_all_discussions_are_resolved]
- attrs = map_public_to_visibility_level(attrs)
- @project = ::Projects::CreateService.new(user, attrs).execute
- if @project.saved?
- present @project, with: Entities::Project,
- user_can_admin_project: can?(current_user, :admin_project, @project)
+ 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: Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, project)
else
- render_validation_error!(@project)
+ 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 Entities::ProjectWithAccess
+ end
+ get ":id" do
+ entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
+ present user_project, with: entity, user: current_user,
+ user_can_admin_project: can?(current_user, :admin_project, user_project)
+ end
+
+ desc 'Get events for a single project' do
+ success Entities::Event
+ end
+ params do
+ use :pagination
+ end
+ get ":id/events" do
+ present paginate(user_project.events.recent), with: Entities::Event
+ end
- # Fork new project for the current user or provided namespace.
- #
- # Parameters:
- # id (required) - The ID of a project
- # namespace (optional) - The ID or name of the namespace that the project will be forked into.
- # Example Request
- # POST /projects/fork/:id
+ desc 'Fork new project for the current user or provided namespace.' do
+ success 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
- attrs = {}
- namespace_id = params[:namespace]
+ fork_params = declared_params(include_missing: false)
+ namespace_id = fork_params[:namespace]
if namespace_id.present?
- namespace = Namespace.find_by(id: namespace_id) || Namespace.find_by_path_or_name(namespace_id)
+ fork_params[:namespace] = if namespace_id =~ /^\d+$/
+ Namespace.find_by(id: namespace_id)
+ else
+ Namespace.find_by_path_or_name(namespace_id)
+ end
- unless namespace && can?(current_user, :create_projects, namespace)
+ unless fork_params[:namespace] && can?(current_user, :create_projects, fork_params[:namespace])
not_found!('Target Namespace')
end
-
- attrs[:namespace] = namespace
end
- @forked_project =
- ::Projects::ForkService.new(user_project,
- current_user,
- attrs).execute
+ forked_project = ::Projects::ForkService.new(user_project, current_user, fork_params).execute
- if @forked_project.errors.any?
- conflict!(@forked_project.errors.messages)
+ if forked_project.errors.any?
+ conflict!(forked_project.errors.messages)
else
- present @forked_project, with: Entities::Project,
- user_can_admin_project: can?(current_user, :admin_project, @forked_project)
+ present forked_project, with: Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, forked_project)
end
end
- # Update an existing project
- #
- # Parameters:
- # id (required) - the id of a project
- # name (optional) - name of a project
- # path (optional) - path of a project
- # description (optional) - short project description
- # issues_enabled (optional)
- # merge_requests_enabled (optional)
- # builds_enabled (optional)
- # wiki_enabled (optional)
- # snippets_enabled (optional)
- # container_registry_enabled (optional)
- # shared_runners_enabled (optional)
- # public (optional) - if true same as setting visibility_level = 20
- # visibility_level (optional) - visibility level of a project
- # public_builds (optional)
- # lfs_enabled (optional)
- # Example Request
- # PUT /projects/:id
+ desc 'Update an existing project' do
+ success 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
- attrs = attributes_for_keys [:builds_enabled,
- :container_registry_enabled,
- :default_branch,
- :description,
- :issues_enabled,
- :lfs_enabled,
- :merge_requests_enabled,
- :name,
- :only_allow_merge_if_build_succeeds,
- :path,
- :public,
- :public_builds,
- :request_access_enabled,
- :shared_runners_enabled,
- :snippets_enabled,
- :visibility_level,
- :wiki_enabled,
- :only_allow_merge_if_all_discussions_are_resolved]
- attrs = map_public_to_visibility_level(attrs)
authorize_admin_project
+ attrs = map_public_to_visibility_level(declared_params(include_missing: false))
authorize! :rename_project, user_project if attrs[:name].present?
- if attrs[:visibility_level].present?
- authorize! :change_visibility_level, user_project
- end
+ authorize! :change_visibility_level, user_project if attrs[:visibility_level].present?
- ::Projects::UpdateService.new(user_project,
- current_user, attrs).execute
+ ::Projects::UpdateService.new(user_project, current_user, attrs).execute
if user_project.errors.any?
render_validation_error!(user_project)
@@ -297,12 +299,9 @@ module API
end
end
- # Archive project
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # PUT /projects/:id/archive
+ desc 'Archive a project' do
+ success Entities::Project
+ end
post ':id/archive' do
authorize!(:archive_project, user_project)
@@ -311,12 +310,9 @@ module API
present user_project, with: Entities::Project
end
- # Unarchive project
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # PUT /projects/:id/unarchive
+ desc 'Unarchive a project' do
+ success Entities::Project
+ end
post ':id/unarchive' do
authorize!(:archive_project, user_project)
@@ -325,12 +321,9 @@ module API
present user_project, with: Entities::Project
end
- # Star project
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # POST /projects/:id/star
+ desc 'Star a project' do
+ success Entities::Project
+ end
post ':id/star' do
if current_user.starred?(user_project)
not_modified!
@@ -342,12 +335,9 @@ module API
end
end
- # Unstar project
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # DELETE /projects/:id/star
+ desc 'Unstar a project' do
+ success Entities::Project
+ end
delete ':id/star' do
if current_user.starred?(user_project)
current_user.toggle_star(user_project)
@@ -359,67 +349,51 @@ module API
end
end
- # Remove project
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # DELETE /projects/:id
+ desc 'Remove a project'
delete ":id" do
authorize! :remove_project, user_project
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
end
- # Mark this project as forked from another
- #
- # Parameters:
- # id: (required) - The ID of the project being marked as a fork
- # forked_from_id: (required) - The ID of the project it was forked from
- # Example Request:
- # POST /projects/:id/fork/:forked_from_id
+ 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])
- unless forked_from_project.nil?
- 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
+
+ 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
- not_found!("Source Project")
+ render_api_error!("Project already forked", 409)
end
end
- # Remove a forked_from relationship
- #
- # Parameters:
- # id: (required) - The ID of the project being marked as a fork
- # Example Request:
- # DELETE /projects/:id/fork
+ 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
- # Share project with group
- #
- # Parameters:
- # id (required) - The ID of a project
- # group_id (required) - The ID of a group
- # group_access (required) - Level of permissions for sharing
- # expires_at (optional) - Share expiration date
- #
- # Example Request:
- # POST /projects/:id/share
+ desc 'Share the project with a group' do
+ success 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
- required_attributes! [:group_id, :group_access]
- attrs = attributes_for_keys [:group_id, :group_access, :expires_at]
-
- group = Group.find_by_id(attrs[:group_id])
+ group = Group.find_by_id(params[:group_id])
unless group && can?(current_user, :read_group, group)
not_found!('Group')
@@ -429,7 +403,7 @@ module API
return render_api_error!("The project sharing with group is disabled", 400)
end
- link = user_project.project_group_links.new(attrs)
+ link = user_project.project_group_links.new(declared_params(include_missing: false))
if link.save
present link, with: Entities::ProjectGroupLink
@@ -438,40 +412,39 @@ module API
end
end
- # Upload a file
- #
- # Parameters:
- # id: (required) - The ID of the project
- # file: (required) - The file to be uploaded
- post ":id/uploads" do
- ::Projects::UploadService.new(user_project, params[:file]).execute
+ 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
- # search for projects current_user has access to
- #
- # Parameters:
- # query (required) - A string contained in the project name
- # per_page (optional) - number of projects to return per page
- # page (optional) - the page to retrieve
- # Example Request:
- # GET /projects/search/:query
- get "/search/:query" do
- search_service = Search::GlobalService.new(current_user, search: params[:query]).execute
- projects = search_service.objects('projects', params[:page])
- projects = projects.reorder(project_order_by => project_sort)
+ link = user_project.project_group_links.find_by(group_id: params[:group_id])
+ not_found!('Group Link') unless link
- present paginate(projects), with: Entities::Project
+ 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
- # Get a users list
- #
- # Example Request:
- # GET /users
+ desc 'Get the users list of a project' do
+ success 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.where(id: user_project.team.users.map(&:id))
- @users = @users.search(params[:search]) if params[:search].present?
- @users = paginate @users
- present @users, with: Entities::UserBasic
+ users = user_project.team.users
+ users = users.search(params[:search]) if params[:search].present?
+
+ present paginate(users), with: Entities::UserBasic
end
end
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index f55aceed92c..c287ee34a68 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -1,11 +1,13 @@
require 'mime/types'
module API
- # Projects API
class Repositories < Grape::API
before { authenticate! }
before { authorize! :download_code, user_project }
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
helpers do
def handle_project_member_errors(errors)
@@ -16,13 +18,14 @@ module API
end
end
- # Get a project repository tree
- #
- # Parameters:
- # id (required) - The ID of a project
- # ref_name (optional) - The name of a repository branch or tag, if not given the default branch is used
- # Example Request:
- # GET /projects/:id/repository/tree
+ desc 'Get a project repository tree' do
+ success Entities::RepoTreeObject
+ end
+ params do
+ optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
+ optional :path, type: String, desc: 'The path of the tree'
+ optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree'
+ end
get ':id/repository/tree' do
ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
path = params[:path] || nil
@@ -30,27 +33,20 @@ module API
commit = user_project.commit(ref)
not_found!('Tree') unless commit
- tree = user_project.repository.tree(commit.id, path)
+ tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive])
present tree.sorted_entries, with: Entities::RepoTreeObject
end
- # Get a raw file contents
- #
- # Parameters:
- # id (required) - The ID of a project
- # sha (required) - The commit or branch name
- # filepath (required) - The path to the file to display
- # Example Request:
- # GET /projects/:id/repository/blobs/:sha
+ desc 'Get a raw file contents'
+ params do
+ requires :sha, type: String, desc: 'The commit, branch name, or tag name'
+ requires :filepath, type: String, desc: 'The path to the file to display'
+ end
get [ ":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob" ] do
- required_attributes! [:filepath]
-
- ref = params[:sha]
-
repo = user_project.repository
- commit = repo.commit(ref)
+ commit = repo.commit(params[:sha])
not_found! "Commit" unless commit
blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath])
@@ -59,20 +55,15 @@ module API
send_git_blob repo, blob
end
- # Get a raw blob contents by blob sha
- #
- # Parameters:
- # id (required) - The ID of a project
- # sha (required) - The blob's sha
- # Example Request:
- # GET /projects/:id/repository/raw_blobs/:sha
+ desc 'Get a raw blob contents by blob sha'
+ params do
+ requires :sha, type: String, desc: 'The commit, branch name, or tag name'
+ end
get ':id/repository/raw_blobs/:sha' do
- ref = params[:sha]
-
repo = user_project.repository
begin
- blob = Gitlab::Git::Blob.raw(repo, ref)
+ blob = Gitlab::Git::Blob.raw(repo, params[:sha])
rescue
not_found! 'Blob'
end
@@ -82,15 +73,12 @@ module API
send_git_blob repo, blob
end
- # Get a an archive of the repository
- #
- # Parameters:
- # id (required) - The ID of a project
- # sha (optional) - the commit sha to download defaults to the tip of the default branch
- # Example Request:
- # GET /projects/:id/repository/archive
- get ':id/repository/archive',
- requirements: { format: Gitlab::Regex.archive_formats_regex } do
+ desc 'Get an archive of the repository'
+ params do
+ optional :sha, type: String, desc: 'The commit sha of the archive to be downloaded'
+ optional :format, type: String, desc: 'The archive format'
+ end
+ get ':id/repository/archive', requirements: { format: Gitlab::Regex.archive_formats_regex } do
authorize! :download_code, user_project
begin
@@ -100,27 +88,22 @@ module API
end
end
- # Compare two branches, tags or commits
- #
- # Parameters:
- # id (required) - The ID of a project
- # from (required) - the commit sha or branch name
- # to (required) - the commit sha or branch name
- # Example Request:
- # GET /projects/:id/repository/compare?from=master&to=feature
+ desc 'Compare two branches, tags, or commits' do
+ success Entities::Compare
+ end
+ params do
+ requires :from, type: String, desc: 'The commit, branch name, or tag name to start comparison'
+ requires :to, type: String, desc: 'The commit, branch name, or tag name to stop comparison'
+ end
get ':id/repository/compare' do
authorize! :download_code, user_project
- required_attributes! [:from, :to]
compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to])
present compare, with: Entities::Compare
end
- # Get repository contributors
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # GET /projects/:id/repository/contributors
+ desc 'Get repository contributors' do
+ success Entities::Contributor
+ end
get ':id/repository/contributors' do
authorize! :download_code, user_project
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index ecc8f2fc5a2..4816b5ed1b7 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -1,34 +1,43 @@
module API
- # Runners API
class Runners < Grape::API
+ include PaginationParams
+
before { authenticate! }
resource :runners do
- # Get runners available for user
- #
- # Example Request:
- # GET /runners
+ desc 'Get runners available for user' do
+ success Entities::Runner
+ end
+ params do
+ optional :scope, type: String, values: %w[active paused online],
+ desc: 'The scope of specific runners to show'
+ use :pagination
+ end
get do
runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: ['specific', 'shared'])
present paginate(runners), with: Entities::Runner
end
- # Get all runners - shared and specific
- #
- # Example Request:
- # GET /runners/all
+ desc 'Get all runners - shared and specific' do
+ success Entities::Runner
+ end
+ params do
+ optional :scope, type: String, values: %w[active paused online specific shared],
+ desc: 'The scope of specific runners to show'
+ use :pagination
+ end
get 'all' do
authenticated_as_admin!
runners = filter_runners(Ci::Runner.all, params[:scope])
present paginate(runners), with: Entities::Runner
end
- # Get runner's details
- #
- # Parameters:
- # id (required) - The ID of ther runner
- # Example Request:
- # GET /runners/:id
+ desc "Get runner's details" do
+ success Entities::RunnerDetails
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the runner'
+ end
get ':id' do
runner = get_runner(params[:id])
authenticate_show_runner!(runner)
@@ -36,33 +45,35 @@ module API
present runner, with: Entities::RunnerDetails, current_user: current_user
end
- # Update runner's details
- #
- # Parameters:
- # id (required) - The ID of ther runner
- # description (optional) - Runner's description
- # active (optional) - Runner's status
- # tag_list (optional) - Array of tags for runner
- # Example Request:
- # PUT /runners/:id
+ desc "Update runner's details" do
+ success Entities::RunnerDetails
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the runner'
+ optional :description, type: String, desc: 'The description of the runner'
+ optional :active, type: Boolean, desc: 'The state of a runner'
+ optional :tag_list, type: Array[String], desc: 'The list of tags for a runner'
+ optional :run_untagged, type: Boolean, desc: 'Flag indicating the runner can execute untagged jobs'
+ optional :locked, type: Boolean, desc: 'Flag indicating the runner is locked'
+ at_least_one_of :description, :active, :tag_list, :run_untagged, :locked
+ end
put ':id' do
- runner = get_runner(params[:id])
+ runner = get_runner(params.delete(:id))
authenticate_update_runner!(runner)
- attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged, :locked]
- if runner.update(attrs)
+ if runner.update(declared_params(include_missing: false))
present runner, with: Entities::RunnerDetails, current_user: current_user
else
render_validation_error!(runner)
end
end
- # Remove runner
- #
- # Parameters:
- # id (required) - The ID of ther runner
- # Example Request:
- # DELETE /runners/:id
+ desc 'Remove a runner' do
+ success Entities::Runner
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the runner'
+ end
delete ':id' do
runner = get_runner(params[:id])
authenticate_delete_runner!(runner)
@@ -72,28 +83,32 @@ module API
end
end
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
before { authorize_admin_project }
- # Get runners available for project
- #
- # Example Request:
- # GET /projects/:id/runners
+ desc 'Get runners available for project' do
+ success Entities::Runner
+ end
+ params do
+ optional :scope, type: String, values: %w[active paused online specific shared],
+ desc: 'The scope of specific runners to show'
+ use :pagination
+ end
get ':id/runners' do
runners = filter_runners(Ci::Runner.owned_or_shared(user_project.id), params[:scope])
present paginate(runners), with: Entities::Runner
end
- # Enable runner for project
- #
- # Parameters:
- # id (required) - The ID of the project
- # runner_id (required) - The ID of the runner
- # Example Request:
- # POST /projects/:id/runners/:runner_id
+ desc 'Enable a runner for a project' do
+ success Entities::Runner
+ end
+ params do
+ requires :runner_id, type: Integer, desc: 'The ID of the runner'
+ end
post ':id/runners' do
- required_attributes! [:runner_id]
-
runner = get_runner(params[:runner_id])
authenticate_enable_runner!(runner)
@@ -106,13 +121,12 @@ module API
end
end
- # Disable project's runner
- #
- # Parameters:
- # id (required) - The ID of the project
- # runner_id (required) - The ID of the runner
- # Example Request:
- # DELETE /projects/:id/runners/:runner_id
+ desc "Disable project's runner" do
+ success Entities::Runner
+ end
+ params do
+ requires :runner_id, type: Integer, desc: 'The ID of the runner'
+ end
delete ':id/runners/:runner_id' do
runner_project = user_project.runner_projects.find_by(runner_id: params[:runner_id])
not_found!('Runner') unless runner_project
diff --git a/lib/api/services.rb b/lib/api/services.rb
index fc8598daa32..59232c84c24 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -1,63 +1,640 @@
module API
- # Projects API
class Services < Grape::API
- before { authenticate! }
- before { authorize_admin_project }
+ services = {
+ 'asana' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'User API token'
+ },
+ {
+ required: false,
+ name: :restrict_to_branch,
+ type: String,
+ desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches'
+ }
+ ],
+ 'assembla' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The authentication token'
+ },
+ {
+ required: false,
+ name: :subdomain,
+ type: String,
+ desc: 'Subdomain setting'
+ }
+ ],
+ 'bamboo' => [
+ {
+ required: true,
+ name: :bamboo_url,
+ type: String,
+ desc: 'Bamboo root URL like https://bamboo.example.com'
+ },
+ {
+ required: true,
+ name: :build_key,
+ type: String,
+ desc: 'Bamboo build plan key like'
+ },
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'A user with API access, if applicable'
+ },
+ {
+ required: true,
+ name: :password,
+ type: String,
+ desc: 'Passord of the user'
+ }
+ ],
+ 'bugzilla' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'New issue URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'Issues URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'Project URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'Description'
+ },
+ {
+ required: false,
+ name: :title,
+ type: String,
+ desc: 'Title'
+ }
+ ],
+ 'buildkite' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Buildkite project GitLab token'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'The buildkite project URL'
+ },
+ {
+ required: false,
+ name: :enable_ssl_verification,
+ type: Boolean,
+ desc: 'Enable SSL verification for communication'
+ }
+ ],
+ 'builds-email' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Comma-separated list of recipient email addresses'
+ },
+ {
+ required: false,
+ name: :add_pusher,
+ type: Boolean,
+ desc: 'Add pusher to recipients list'
+ },
+ {
+ required: false,
+ name: :notify_only_broken_builds,
+ type: Boolean,
+ desc: 'Notify only broken builds'
+ }
+ ],
+ 'campfire' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Campfire token'
+ },
+ {
+ required: false,
+ name: :subdomain,
+ type: String,
+ desc: 'Campfire subdomain'
+ },
+ {
+ required: false,
+ name: :room,
+ type: String,
+ desc: 'Campfire room'
+ },
+ ],
+ 'custom-issue-tracker' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'New issue URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'Issues URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'Project URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'Description'
+ },
+ {
+ required: false,
+ name: :title,
+ type: String,
+ desc: 'Title'
+ }
+ ],
+ 'drone-ci' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Drone CI token'
+ },
+ {
+ required: true,
+ name: :drone_url,
+ type: String,
+ desc: 'Drone CI URL'
+ },
+ {
+ required: false,
+ name: :enable_ssl_verification,
+ type: Boolean,
+ desc: 'Enable SSL verification for communication'
+ }
+ ],
+ 'emails-on-push' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Comma-separated list of recipient email addresses'
+ },
+ {
+ required: false,
+ name: :disable_diffs,
+ type: Boolean,
+ desc: 'Disable code diffs'
+ },
+ {
+ required: false,
+ name: :send_from_committer_email,
+ type: Boolean,
+ desc: 'Send from committer'
+ }
+ ],
+ 'external-wiki' => [
+ {
+ required: true,
+ name: :external_wiki_url,
+ type: String,
+ desc: 'The URL of the external Wiki'
+ }
+ ],
+ 'flowdock' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Flowdock token'
+ }
+ ],
+ 'gemnasium' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'Your personal API key on gemnasium.com'
+ },
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: "The project's slug on gemnasium.com"
+ }
+ ],
+ 'hipchat' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The room token'
+ },
+ {
+ required: false,
+ name: :room,
+ type: String,
+ desc: 'The room name or ID'
+ },
+ {
+ required: false,
+ name: :color,
+ type: String,
+ desc: 'The room color'
+ },
+ {
+ required: false,
+ name: :notify,
+ type: Boolean,
+ desc: 'Enable notifications'
+ },
+ {
+ required: false,
+ name: :api_version,
+ type: String,
+ desc: 'Leave blank for default (v2)'
+ },
+ {
+ required: false,
+ name: :server,
+ type: String,
+ desc: 'Leave blank for default. https://hipchat.example.com'
+ }
+ ],
+ 'irker' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Recipients/channels separated by whitespaces'
+ },
+ {
+ required: false,
+ name: :default_irc_uri,
+ type: String,
+ desc: 'Default: irc://irc.network.net:6697'
+ },
+ {
+ required: false,
+ name: :server_host,
+ type: String,
+ desc: 'Server host. Default localhost'
+ },
+ {
+ required: false,
+ name: :server_port,
+ type: Integer,
+ desc: 'Server port. Default 6659'
+ },
+ {
+ required: false,
+ name: :colorize_messages,
+ type: Boolean,
+ desc: 'Colorize messages'
+ }
+ ],
+ 'jira' => [
+ {
+ required: true,
+ name: :url,
+ type: String,
+ desc: 'The URL to the JIRA project which is being linked to this GitLab project, e.g., https://jira.example.com'
+ },
+ {
+ required: true,
+ name: :project_key,
+ type: String,
+ desc: 'The short identifier for your JIRA project, all uppercase, e.g., PROJ'
+ },
+ {
+ required: false,
+ name: :username,
+ type: String,
+ desc: 'The username of the user created to be used with GitLab/JIRA'
+ },
+ {
+ required: false,
+ name: :password,
+ type: String,
+ desc: 'The password of the user created to be used with GitLab/JIRA'
+ },
+ {
+ required: false,
+ name: :jira_issue_transition_id,
+ type: Integer,
+ desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`'
+ }
+ ],
+
+ 'kubernetes' => [
+ {
+ required: true,
+ name: :namespace,
+ type: String,
+ desc: 'The Kubernetes namespace to use'
+ },
+ {
+ required: true,
+ name: :api_url,
+ type: String,
+ desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com'
+ },
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The service token to authenticate against the Kubernetes cluster with'
+ },
+ {
+ required: false,
+ name: :ca_pem,
+ type: String,
+ desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)'
+ },
+ ],
+
+ 'mattermost-slash-commands' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Mattermost token'
+ }
+ ],
+ 'pipelines-email' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Comma-separated list of recipient email addresses'
+ },
+ {
+ required: false,
+ name: :notify_only_broken_builds,
+ type: Boolean,
+ desc: 'Notify only broken builds'
+ }
+ ],
+ 'pivotaltracker' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Pivotaltracker token'
+ },
+ {
+ required: false,
+ name: :restrict_to_branch,
+ type: String,
+ desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.'
+ }
+ ],
+ 'pushover' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'The application key'
+ },
+ {
+ required: true,
+ name: :user_key,
+ type: String,
+ desc: 'The user key'
+ },
+ {
+ required: true,
+ name: :priority,
+ type: String,
+ desc: 'The priority'
+ },
+ {
+ required: true,
+ name: :device,
+ type: String,
+ desc: 'Leave blank for all active devices'
+ },
+ {
+ required: true,
+ name: :sound,
+ type: String,
+ desc: 'The sound of the notification'
+ }
+ ],
+ 'redmine' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'The new issue URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'The project URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'The issues URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'The description of the tracker'
+ }
+ ],
+ 'slack-notification' => [
+ {
+ required: true,
+ name: :webhook,
+ type: String,
+ desc: 'The Slack webhook. e.g. https://hooks.slack.com/services/...'
+ },
+ {
+ required: false,
+ name: :new_issue_url,
+ type: String,
+ desc: 'The user name'
+ },
+ {
+ required: false,
+ name: :channel,
+ type: String,
+ desc: 'The channel name'
+ }
+ ],
+ 'mattermost-notification' => [
+ {
+ required: true,
+ name: :webhook,
+ type: String,
+ desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...'
+ }
+ ],
+ 'teamcity' => [
+ {
+ required: true,
+ name: :teamcity_url,
+ type: String,
+ desc: 'TeamCity root URL like https://teamcity.example.com'
+ },
+ {
+ required: true,
+ name: :build_type,
+ type: String,
+ desc: 'Build configuration ID'
+ },
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'A user with permissions to trigger a manual build'
+ },
+ {
+ required: true,
+ name: :password,
+ type: String,
+ desc: 'The password of the user'
+ }
+ ]
+ }.freeze
+
+ trigger_services = {
+ 'mattermost-slash-commands' => [
+ {
+ name: :token,
+ type: String,
+ desc: 'The Mattermost token'
+ }
+ ]
+ }.freeze
resource :projects do
- # Set <service_slug> service for project
- #
- # Example Request:
- #
- # PUT /projects/:id/services/gitlab-ci
- #
- put ':id/services/:service_slug' do
- if project_service
- validators = project_service.class.validators.select do |s|
- s.class == ActiveRecord::Validations::PresenceValidator &&
- s.attributes != [:project_id]
+ before { authenticate! }
+ before { authorize_admin_project }
+
+ helpers do
+ def service_attributes(service)
+ service.fields.inject([]) do |arr, hash|
+ arr << hash[:name].to_sym
end
+ end
+ end
- required_attributes! validators.map(&:attributes).flatten.uniq
- attrs = attributes_for_keys service_attributes
+ services.each do |service_slug, settings|
+ desc "Set #{service_slug} service for project"
+ params do
+ settings.each do |setting|
+ if setting[:required]
+ requires setting[:name], type: setting[:type], desc: setting[:desc]
+ else
+ optional setting[:name], type: setting[:type], desc: setting[:desc]
+ end
+ end
+ end
+ put ":id/services/#{service_slug}" do
+ service = user_project.find_or_initialize_service(service_slug.underscore)
+ service_params = declared_params(include_missing: false).merge(active: true)
- if project_service.update_attributes(attrs.merge(active: true))
+ if service.update_attributes(service_params)
true
else
- not_found!
+ render_api_error!('400 Bad Request', 400)
end
end
end
- # Delete <service_slug> service for project
- #
- # Example Request:
- #
- # DELETE /project/:id/services/gitlab-ci
- #
- delete ':id/services/:service_slug' do
- if project_service
- attrs = service_attributes.inject({}) do |hash, key|
- hash.merge!(key => nil)
+ desc "Delete a service for project"
+ params do
+ requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
+ end
+ delete ":id/services/:service_slug" do
+ service = user_project.find_or_initialize_service(params[:service_slug].underscore)
+
+ attrs = service_attributes(service).inject({}) do |hash, key|
+ hash.merge!(key => nil)
+ end
+
+ if service.update_attributes(attrs.merge(active: false))
+ true
+ else
+ render_api_error!('400 Bad Request', 400)
+ end
+ end
+
+ desc 'Get the service settings for project' do
+ success Entities::ProjectService
+ end
+ params do
+ requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
+ end
+ get ":id/services/:service_slug" do
+ service = user_project.find_or_initialize_service(params[:service_slug].underscore)
+ present service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
+ end
+ end
+
+ trigger_services.each do |service_slug, settings|
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ desc "Trigger a slash command for #{service_slug}" do
+ detail 'Added in GitLab 8.13'
+ end
+ params do
+ settings.each do |setting|
+ requires setting[:name], type: setting[:type], desc: setting[:desc]
end
+ end
+ post ":id/services/#{service_slug.underscore}/trigger" do
+ project = find_project(params[:id])
- if project_service.update_attributes(attrs.merge(active: false))
- true
+ # 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)
+
+ if result
+ status result[:status] || 200
+ present result
else
- not_found!
+ not_found!('Service')
end
end
end
-
- # Get <service_slug> service settings for project
- #
- # Example Request:
- #
- # GET /project/:id/services/gitlab-ci
- #
- get ':id/services/:service_slug' do
- present project_service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
- end
end
end
end
diff --git a/lib/api/session.rb b/lib/api/session.rb
index 55ec66a6d67..002ffd1d154 100644
--- a/lib/api/session.rb
+++ b/lib/api/session.rb
@@ -1,21 +1,20 @@
module API
- # Users API
class Session < Grape::API
- # Login to get token
- #
- # Parameters:
- # login (*required) - user login
- # email (*required) - user email
- # password (required) - user password
- #
- # Example Request:
- # POST /session
+ desc 'Login to get token' do
+ success Entities::UserWithPrivateToken
+ end
+ params do
+ optional :login, type: String, desc: 'The username'
+ optional :email, type: String, desc: 'The email of the user'
+ requires :password, type: String, desc: 'The password of the user'
+ at_least_one_of :login, :email
+ end
post "/session" do
user = Gitlab::Auth.find_with_user_password(params[:email] || params[:login], params[:password])
return unauthorized! unless user
return render_api_error!('401 Unauthorized. You have 2FA enabled. Please use a personal access token to access the API', 401) if user.two_factor_enabled?
- present user, with: Entities::UserLogin
+ present user, with: Entities::UserWithPrivateToken
end
end
end
diff --git a/lib/api/sidekiq_metrics.rb b/lib/api/sidekiq_metrics.rb
index d3d6827dc54..11f2b40269a 100644
--- a/lib/api/sidekiq_metrics.rb
+++ b/lib/api/sidekiq_metrics.rb
@@ -39,50 +39,22 @@ module API
end
end
- # Get Sidekiq Queue metrics
- #
- # Parameters:
- # None
- #
- # Example:
- # GET /sidekiq/queue_metrics
- #
+ desc 'Get the Sidekiq queue metrics'
get 'sidekiq/queue_metrics' do
{ queues: queue_metrics }
end
- # Get Sidekiq Process metrics
- #
- # Parameters:
- # None
- #
- # Example:
- # GET /sidekiq/process_metrics
- #
+ desc 'Get the Sidekiq process metrics'
get 'sidekiq/process_metrics' do
{ processes: process_metrics }
end
- # Get Sidekiq Job statistics
- #
- # Parameters:
- # None
- #
- # Example:
- # GET /sidekiq/job_stats
- #
+ desc 'Get the Sidekiq job statistics'
get 'sidekiq/job_stats' do
{ jobs: job_stats }
end
- # Get Sidekiq Compound metrics. Includes all previous metrics
- #
- # Parameters:
- # None
- #
- # Example:
- # GET /sidekiq/compound_metrics
- #
+ desc 'Get the Sidekiq Compound metrics. Includes queue, process, and job statistics'
get 'sidekiq/compound_metrics' do
{ queues: queue_metrics, processes: process_metrics, jobs: job_stats }
end
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
new file mode 100644
index 00000000000..e096e636806
--- /dev/null
+++ b/lib/api/snippets.rb
@@ -0,0 +1,137 @@
+module API
+ # Snippets API
+ class Snippets < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ resource :snippets do
+ helpers do
+ def snippets_for_current_user
+ SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user)
+ end
+
+ def public_snippets
+ SnippetsFinder.new.execute(current_user, filter: :public)
+ end
+ end
+
+ desc 'Get a snippets list for authenticated user' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success Entities::PersonalSnippet
+ end
+ params do
+ use :pagination
+ end
+ get do
+ present paginate(snippets_for_current_user), with: Entities::PersonalSnippet
+ end
+
+ desc 'List all public snippets current_user has access to' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success Entities::PersonalSnippet
+ end
+ params do
+ use :pagination
+ end
+ get 'public' do
+ present paginate(public_snippets), with: Entities::PersonalSnippet
+ end
+
+ desc 'Get a single snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success Entities::PersonalSnippet
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ end
+ get ':id' do
+ snippet = snippets_for_current_user.find(params[:id])
+ present snippet, with: Entities::PersonalSnippet
+ end
+
+ desc 'Create new snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success Entities::PersonalSnippet
+ end
+ params do
+ requires :title, type: String, desc: 'The title of a snippet'
+ requires :file_name, type: String, desc: 'The name of a snippet file'
+ requires :content, type: String, desc: 'The content of a snippet'
+ optional :visibility_level, type: Integer,
+ values: Gitlab::VisibilityLevel.values,
+ default: Gitlab::VisibilityLevel::INTERNAL,
+ desc: 'The visibility level of the snippet'
+ end
+ post do
+ attrs = declared_params(include_missing: false)
+ snippet = CreateSnippetService.new(nil, current_user, attrs).execute
+
+ if snippet.persisted?
+ present snippet, with: Entities::PersonalSnippet
+ else
+ render_validation_error!(snippet)
+ end
+ end
+
+ desc 'Update an existing snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success Entities::PersonalSnippet
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ optional :title, type: String, desc: 'The title of a snippet'
+ optional :file_name, type: String, desc: 'The name of a snippet file'
+ optional :content, type: String, desc: 'The content of a snippet'
+ optional :visibility_level, type: Integer,
+ values: Gitlab::VisibilityLevel.values,
+ desc: 'The visibility level of the snippet'
+ at_least_one_of :title, :file_name, :content, :visibility_level
+ end
+ put ':id' do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+ return not_found!('Snippet') unless snippet
+ authorize! :update_personal_snippet, snippet
+
+ attrs = declared_params(include_missing: false)
+
+ UpdateSnippetService.new(nil, current_user, snippet, attrs).execute
+ if snippet.persisted?
+ present snippet, with: Entities::PersonalSnippet
+ else
+ render_validation_error!(snippet)
+ end
+ end
+
+ desc 'Remove snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success Entities::PersonalSnippet
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ end
+ delete ':id' do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+ return not_found!('Snippet') unless snippet
+ authorize! :destroy_personal_snippet, snippet
+ snippet.destroy
+ no_content!
+ end
+
+ desc 'Get a raw snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ end
+ get ":id/raw" do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+ return not_found!('Snippet') unless snippet
+
+ env['api.format'] = :txt
+ content_type 'text/plain'
+ present snippet.content
+ end
+ end
+ end
+end
diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb
index c49e2a21b82..10749b34004 100644
--- a/lib/api/subscriptions.rb
+++ b/lib/api/subscriptions.rb
@@ -9,49 +9,40 @@ module API
'labels' => proc { |id| find_project_label(id) },
}
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :subscribable_id, type: String, desc: 'The ID of a resource'
+ end
resource :projects do
subscribable_types.each do |type, finder|
type_singularized = type.singularize
- type_id_str = :"#{type_singularized}_id"
entity_class = Entities.const_get(type_singularized.camelcase)
- # Subscribe to a resource
- #
- # Parameters:
- # id (required) - The ID of a project
- # subscribable_id (required) - The ID of a resource
- # Example Request:
- # POST /projects/:id/labels/:subscribable_id/subscription
- # POST /projects/:id/issues/:subscribable_id/subscription
- # POST /projects/:id/merge_requests/:subscribable_id/subscription
- post ":id/#{type}/:#{type_id_str}/subscription" do
- resource = instance_exec(params[type_id_str], &finder)
+ desc 'Subscribe to a resource' do
+ success entity_class
+ end
+ post ":id/#{type}/:subscribable_id/subscription" do
+ resource = instance_exec(params[:subscribable_id], &finder)
- if resource.subscribed?(current_user)
+ if resource.subscribed?(current_user, user_project)
not_modified!
else
- resource.subscribe(current_user)
- present resource, with: entity_class, current_user: current_user
+ resource.subscribe(current_user, user_project)
+ present resource, with: entity_class, current_user: current_user, project: user_project
end
end
- # Unsubscribe from a resource
- #
- # Parameters:
- # id (required) - The ID of a project
- # subscribable_id (required) - The ID of a resource
- # Example Request:
- # DELETE /projects/:id/labels/:subscribable_id/subscription
- # DELETE /projects/:id/issues/:subscribable_id/subscription
- # DELETE /projects/:id/merge_requests/:subscribable_id/subscription
- delete ":id/#{type}/:#{type_id_str}/subscription" do
- resource = instance_exec(params[type_id_str], &finder)
+ desc 'Unsubscribe from a resource' do
+ success entity_class
+ end
+ delete ":id/#{type}/:subscribable_id/subscription" do
+ resource = instance_exec(params[:subscribable_id], &finder)
- if !resource.subscribed?(current_user)
+ if !resource.subscribed?(current_user, user_project)
not_modified!
else
- resource.unsubscribe(current_user)
- present resource, with: entity_class, current_user: current_user
+ resource.unsubscribe(current_user, user_project)
+ present resource, with: entity_class, current_user: current_user, project: user_project
end
end
end
diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb
index b6bfff9f20f..708ec8cfe70 100644
--- a/lib/api/system_hooks.rb
+++ b/lib/api/system_hooks.rb
@@ -27,7 +27,7 @@ module API
optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook"
end
post do
- hook = SystemHook.new declared(params, include_missing: false).to_h
+ hook = SystemHook.new(declared_params(include_missing: false))
if hook.save
present hook, with: Entities::Hook
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index bf2a199ce21..5b345db3a41 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -1,7 +1,6 @@
module API
# Git Tags API
class Tags < Grape::API
- before { authenticate! }
before { authorize! :download_code, user_project }
params do
@@ -40,10 +39,9 @@ module API
end
post ':id/repository/tags' do
authorize_push_project
- create_params = declared(params)
result = CreateTagService.new(user_project, current_user).
- execute(create_params[:tag_name], create_params[:ref], create_params[:message], create_params[:release_description])
+ execute(params[:tag_name], params[:ref], params[:message], params[:release_description])
if result[:status] == :success
present result[:tag],
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index 832b04a3bb1..ed8f48aa1e3 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -1,6 +1,7 @@
module API
- # Todos API
class Todos < Grape::API
+ include PaginationParams
+
before { authenticate! }
ISSUABLE_TYPES = {
@@ -44,10 +45,11 @@ module API
desc 'Get a todo list' do
success Entities::Todo
end
+ params do
+ use :pagination
+ end
get do
- todos = find_todos
-
- present paginate(todos), with: Entities::Todo, current_user: current_user
+ present paginate(find_todos), with: Entities::Todo, current_user: current_user
end
desc 'Mark a todo as done' do
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index d1d07394e92..87a717ba751 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -1,20 +1,21 @@
module API
- # Triggers API
class Triggers < Grape::API
+ include PaginationParams
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
- # Trigger a GitLab project build
- #
- # Parameters:
- # id (required) - The ID of a CI project
- # ref (required) - The name of project's branch or tag
- # token (required) - The uniq token of trigger
- # variables (optional) - The list of variables to be injected into build
- # Example Request:
- # POST /projects/:id/trigger/builds
- post ":id/trigger/builds" do
- required_attributes! [:ref, :token]
-
- project = Project.find_with_namespace(params[:id]) || Project.find_by(id: params[:id])
+ desc 'Trigger a GitLab project build' do
+ success Entities::TriggerRequest
+ end
+ params do
+ requires :ref, type: String, desc: 'The commit sha or name of a branch or tag'
+ requires :token, type: String, desc: 'The unique token of trigger'
+ optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
+ end
+ post ":id/(ref/:ref/)trigger/builds" do
+ project = find_project(params[:id])
trigger = Ci::Trigger.find_by_token(params[:token].to_s)
not_found! unless project && trigger
unauthorized! unless trigger.project == project
@@ -22,10 +23,6 @@ module API
# validate variables
variables = params[:variables]
if variables
- unless variables.is_a?(Hash)
- render_api_error!('variables needs to be a hash', 400)
- end
-
unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
render_api_error!('variables needs to be a map of key-valued strings', 400)
end
@@ -44,31 +41,27 @@ module API
end
end
- # Get triggers list
- #
- # Parameters:
- # id (required) - The ID of a project
- # page (optional) - The page number for pagination
- # per_page (optional) - The value of items per page to show
- # Example Request:
- # GET /projects/:id/triggers
+ desc 'Get triggers list' do
+ success Entities::Trigger
+ end
+ params do
+ use :pagination
+ end
get ':id/triggers' do
authenticate!
authorize! :admin_build, user_project
triggers = user_project.triggers.includes(:trigger_requests)
- triggers = paginate(triggers)
- present triggers, with: Entities::Trigger
+ present paginate(triggers), with: Entities::Trigger
end
- # Get specific trigger of a project
- #
- # Parameters:
- # id (required) - The ID of a project
- # token (required) - The `token` of a trigger
- # Example Request:
- # GET /projects/:id/triggers/:token
+ desc 'Get specific trigger of a project' do
+ success Entities::Trigger
+ end
+ params do
+ requires :token, type: String, desc: 'The unique token of trigger'
+ end
get ':id/triggers/:token' do
authenticate!
authorize! :admin_build, user_project
@@ -79,12 +72,9 @@ module API
present trigger, with: Entities::Trigger
end
- # Create trigger
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # POST /projects/:id/triggers
+ desc 'Create a trigger' do
+ success Entities::Trigger
+ end
post ':id/triggers' do
authenticate!
authorize! :admin_build, user_project
@@ -94,13 +84,12 @@ module API
present trigger, with: Entities::Trigger
end
- # Delete trigger
- #
- # Parameters:
- # id (required) - The ID of a project
- # token (required) - The `token` of a trigger
- # Example Request:
- # DELETE /projects/:id/triggers/:token
+ desc 'Delete a trigger' do
+ success Entities::Trigger
+ end
+ params do
+ requires :token, type: String, desc: 'The unique token of trigger'
+ end
delete ':id/triggers/:token' do
authenticate!
authorize! :admin_build, user_project
diff --git a/lib/api/users.rb b/lib/api/users.rb
index c28e07a76b7..0842c3874c5 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -1,94 +1,108 @@
module API
- # Users API
class Users < Grape::API
- before { authenticate! }
+ include PaginationParams
+
+ before do
+ allow_access_with_scope :read_user if request.get?
+ authenticate!
+ end
resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
- # Get a users list
- #
- # Example Request:
- # GET /users
- # GET /users?search=Admin
- # GET /users?username=root
+ helpers do
+ params :optional_attributes do
+ optional :skype, type: String, desc: 'The Skype username'
+ optional :linkedin, type: String, desc: 'The LinkedIn username'
+ optional :twitter, type: String, desc: 'The Twitter username'
+ optional :website_url, type: String, desc: 'The website of the user'
+ optional :organization, type: String, desc: 'The organization of the user'
+ optional :projects_limit, type: Integer, desc: 'The number of projects a user can create'
+ optional :extern_uid, type: Integer, desc: 'The external authentication provider UID'
+ optional :provider, type: String, desc: 'The external provider'
+ optional :bio, type: String, desc: 'The biography of the user'
+ optional :location, type: String, desc: 'The location of the user'
+ optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator'
+ optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups'
+ optional :confirm, type: Boolean, desc: 'Flag indicating the account needs to be confirmed'
+ optional :external, type: Boolean, desc: 'Flag indicating the user is an external user'
+ all_or_none_of :extern_uid, :provider
+ end
+ end
+
+ desc 'Get the list of users' do
+ success Entities::UserBasic
+ end
+ params do
+ optional :username, type: String, desc: 'Get a single user with a specific username'
+ optional :search, type: String, desc: 'Search for a username'
+ optional :active, type: Boolean, default: false, desc: 'Filters only active users'
+ optional :external, type: Boolean, default: false, desc: 'Filters only external users'
+ optional :blocked, type: Boolean, default: false, desc: 'Filters only blocked users'
+ use :pagination
+ end
get do
unless can?(current_user, :read_users_list, nil)
render_api_error!("Not authorized.", 403)
end
if params[:username].present?
- @users = User.where(username: params[:username])
+ users = User.where(username: params[:username])
else
- @users = User.all
- @users = @users.active if params[:active].present?
- @users = @users.search(params[:search]) if params[:search].present?
- @users = paginate @users
+ users = User.all
+ users = users.active if params[:active]
+ users = users.search(params[:search]) if params[:search].present?
+ users = users.blocked if params[:blocked]
+ users = users.external if params[:external] && current_user.is_admin?
end
- if current_user.is_admin?
- present @users, with: Entities::UserFull
- else
- present @users, with: Entities::UserBasic
- end
+ entity = current_user.is_admin? ? Entities::UserPublic : Entities::UserBasic
+ present paginate(users), with: entity
end
- # Get a single user
- #
- # Parameters:
- # id (required) - The ID of a user
- # Example Request:
- # GET /users/:id
+ desc 'Get a single user' do
+ success Entities::UserBasic
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ end
get ":id" do
- @user = User.find(params[:id])
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
if current_user && current_user.is_admin?
- present @user, with: Entities::UserFull
- elsif can?(current_user, :read_user, @user)
- present @user, with: Entities::User
+ present user, with: Entities::UserPublic
+ elsif can?(current_user, :read_user, user)
+ present user, with: Entities::User
else
render_api_error!("User not found.", 404)
end
end
- # Create user. Available only for admin
- #
- # Parameters:
- # email (required) - Email
- # password (required) - Password
- # name (required) - Name
- # username (required) - Name
- # skype - Skype ID
- # linkedin - Linkedin
- # twitter - Twitter account
- # website_url - Website url
- # organization - Organization
- # projects_limit - Number of projects user can create
- # extern_uid - External authentication provider UID
- # provider - External provider
- # bio - Bio
- # location - Location of the user
- # admin - User is admin - true or false (default)
- # can_create_group - User can create groups - true or false
- # confirm - Require user confirmation - true (default) or false
- # external - Flags the user as external - true or false(default)
- # Example Request:
- # POST /users
+ desc 'Create a user. Available only for admins.' do
+ success Entities::UserPublic
+ end
+ params do
+ requires :email, type: String, desc: 'The email of the user'
+ requires :password, type: String, desc: 'The password of the new user'
+ requires :name, type: String, desc: 'The name of the user'
+ requires :username, type: String, desc: 'The username of the user'
+ use :optional_attributes
+ end
post do
authenticated_as_admin!
- required_attributes! [:email, :password, :name, :username]
- attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external, :organization]
- admin = attrs.delete(:admin)
- confirm = !(attrs.delete(:confirm) =~ /(false|f|no|0)$/i)
- user = User.build_user(attrs)
- user.admin = admin unless admin.nil?
+
+ # Filter out params which are used later
+ identity_attrs = params.slice(:provider, :extern_uid)
+ confirm = params.delete(:confirm)
+
+ user = User.build_user(declared_params(include_missing: false))
user.skip_confirmation! unless confirm
- identity_attrs = attributes_for_keys [:provider, :extern_uid]
if identity_attrs.any?
user.identities.build(identity_attrs)
end
if user.save
- present user, with: Entities::UserFull
+ present user, with: Entities::UserPublic
else
conflict!('Email has already been taken') if User.
where(email: user.email).
@@ -102,46 +116,41 @@ module API
end
end
- # Update user. Available only for admin
- #
- # Parameters:
- # email - Email
- # name - Name
- # password - Password
- # skype - Skype ID
- # linkedin - Linkedin
- # twitter - Twitter account
- # website_url - Website url
- # organization - Organization
- # projects_limit - Limit projects each user can create
- # bio - Bio
- # location - Location of the user
- # admin - User is admin - true or false (default)
- # can_create_group - User can create groups - true or false
- # external - Flags the user as external - true or false(default)
- # Example Request:
- # PUT /users/:id
+ desc 'Update a user. Available only for admins.' do
+ success Entities::UserPublic
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ optional :email, type: String, desc: 'The email of the user'
+ optional :password, type: String, desc: 'The password of the new user'
+ optional :name, type: String, desc: 'The name of the user'
+ optional :username, type: String, desc: 'The username of the user'
+ use :optional_attributes
+ at_least_one_of :email, :password, :name, :username, :skype, :linkedin,
+ :twitter, :website_url, :organization, :projects_limit,
+ :extern_uid, :provider, :bio, :location, :admin,
+ :can_create_group, :confirm, :external
+ end
put ":id" do
authenticated_as_admin!
- attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :external, :organization]
- user = User.find(params[:id])
+ user = User.find_by(id: params.delete(:id))
not_found!('User') unless user
- admin = attrs.delete(:admin)
- user.admin = admin unless admin.nil?
-
- conflict!('Email has already been taken') if attrs[:email] &&
- User.where(email: attrs[:email]).
+ conflict!('Email has already been taken') if params[:email] &&
+ User.where(email: params[:email]).
where.not(id: user.id).count > 0
- conflict!('Username has already been taken') if attrs[:username] &&
- User.where(username: attrs[:username]).
+ conflict!('Username has already been taken') if params[:username] &&
+ User.where(username: params[:username]).
where.not(id: user.id).count > 0
- identity_attrs = attributes_for_keys [:provider, :extern_uid]
+ user_params = declared_params(include_missing: false)
+ identity_attrs = user_params.slice(:provider, :extern_uid)
+
if identity_attrs.any?
identity = user.identities.find_by(provider: identity_attrs[:provider])
+
if identity
identity.update_attributes(identity_attrs)
else
@@ -150,28 +159,33 @@ module API
end
end
- if user.update_attributes(attrs)
- present user, with: Entities::UserFull
+ # Delete already handled parameters
+ user_params.delete(:extern_uid)
+ user_params.delete(:provider)
+
+ if user.update_attributes(user_params)
+ present user, with: Entities::UserPublic
else
render_validation_error!(user)
end
end
- # Add ssh key to a specified user. Only available to admin users.
- #
- # Parameters:
- # id (required) - The ID of a user
- # key (required) - New SSH Key
- # title (required) - New SSH Key's title
- # Example Request:
- # POST /users/:id/keys
+ desc 'Add an SSH key to a specified user. Available only for admins.' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ requires :key, type: String, desc: 'The new SSH key'
+ requires :title, type: String, desc: 'The title of the new SSH key'
+ end
post ":id/keys" do
authenticated_as_admin!
- required_attributes! [:title, :key]
- user = User.find(params[:id])
- attrs = attributes_for_keys [:title, :key]
- key = user.keys.new attrs
+ user = User.find_by(id: params.delete(:id))
+ not_found!('User') unless user
+
+ key = user.keys.new(declared_params(include_missing: false))
+
if key.save
present key, with: Entities::SSHKey
else
@@ -179,55 +193,55 @@ module API
end
end
- # Get ssh keys of a specified user. Only available to admin users.
- #
- # Parameters:
- # uid (required) - The ID of a user
- # Example Request:
- # GET /users/:uid/keys
- get ':uid/keys' do
+ desc 'Get the SSH keys of a specified user. Available only for admins.' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ end
+ get ':id/keys' do
authenticated_as_admin!
- user = User.find_by(id: params[:uid])
+
+ user = User.find_by(id: params[:id])
not_found!('User') unless user
present user.keys, with: Entities::SSHKey
end
- # Delete existing ssh key of a specified user. Only available to admin
- # users.
- #
- # Parameters:
- # uid (required) - The ID of a user
- # id (required) - SSH Key ID
- # Example Request:
- # DELETE /users/:uid/keys/:id
- delete ':uid/keys/:id' do
+ desc 'Delete an existing SSH key from a specified user. Available only for admins.' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ requires :key_id, type: Integer, desc: 'The ID of the SSH key'
+ end
+ delete ':id/keys/:key_id' do
authenticated_as_admin!
- user = User.find_by(id: params[:uid])
+
+ user = User.find_by(id: params[:id])
not_found!('User') unless user
- begin
- key = user.keys.find params[:id]
- key.destroy
- rescue ActiveRecord::RecordNotFound
- not_found!('Key')
- end
+ key = user.keys.find_by(id: params[:key_id])
+ not_found!('Key') unless key
+
+ present key.destroy, with: Entities::SSHKey
end
- # Add email to a specified user. Only available to admin users.
- #
- # Parameters:
- # id (required) - The ID of a user
- # email (required) - Email address
- # Example Request:
- # POST /users/:id/emails
+ desc 'Add an email address to a specified user. Available only for admins.' do
+ success Entities::Email
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ requires :email, type: String, desc: 'The email of the user'
+ end
post ":id/emails" do
authenticated_as_admin!
- required_attributes! [:email]
- user = User.find(params[:id])
- attrs = attributes_for_keys [:email]
- email = user.emails.new attrs
+ user = User.find_by(id: params.delete(:id))
+ not_found!('User') unless user
+
+ email = user.emails.new(declared_params(include_missing: false))
+
if email.save
NotificationService.new.new_email(email)
present email, with: Entities::Email
@@ -236,101 +250,95 @@ module API
end
end
- # Get emails of a specified user. Only available to admin users.
- #
- # Parameters:
- # uid (required) - The ID of a user
- # Example Request:
- # GET /users/:uid/emails
- get ':uid/emails' do
+ desc 'Get the emails addresses of a specified user. Available only for admins.' do
+ success Entities::Email
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ end
+ get ':id/emails' do
authenticated_as_admin!
- user = User.find_by(id: params[:uid])
+ user = User.find_by(id: params[:id])
not_found!('User') unless user
present user.emails, with: Entities::Email
end
- # Delete existing email of a specified user. Only available to admin
- # users.
- #
- # Parameters:
- # uid (required) - The ID of a user
- # id (required) - Email ID
- # Example Request:
- # DELETE /users/:uid/emails/:id
- delete ':uid/emails/:id' do
+ desc 'Delete an email address of a specified user. Available only for admins.' do
+ success Entities::Email
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ requires :email_id, type: Integer, desc: 'The ID of the email'
+ end
+ delete ':id/emails/:email_id' do
authenticated_as_admin!
- user = User.find_by(id: params[:uid])
+ user = User.find_by(id: params[:id])
not_found!('User') unless user
- begin
- email = user.emails.find params[:id]
- email.destroy
+ email = user.emails.find_by(id: params[:email_id])
+ not_found!('Email') unless email
- user.update_secondary_emails!
- rescue ActiveRecord::RecordNotFound
- not_found!('Email')
- end
+ email.destroy
+ user.update_secondary_emails!
end
- # Delete user. Available only for admin
- #
- # Example Request:
- # DELETE /users/:id
+ desc 'Delete a user. Available only for admins.' do
+ success Entities::Email
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ end
delete ":id" do
authenticated_as_admin!
user = User.find_by(id: params[:id])
+ not_found!('User') unless user
- if user
- DeleteUserService.new(current_user).execute(user)
- else
- not_found!('User')
- end
+ DeleteUserService.new(current_user).execute(user)
end
- # Block user. Available only for admin
- #
- # Example Request:
- # PUT /users/:id/block
+ desc 'Block a user. Available only for admins.'
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ end
put ':id/block' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
+ not_found!('User') unless user
- if !user
- not_found!('User')
- elsif !user.ldap_blocked?
+ if !user.ldap_blocked?
user.block
else
forbidden!('LDAP blocked users cannot be modified by the API')
end
end
- # Unblock user. Available only for admin
- #
- # Example Request:
- # PUT /users/:id/unblock
+ desc 'Unblock a user. Available only for admins.'
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ end
put ':id/unblock' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
+ not_found!('User') unless user
- if !user
- not_found!('User')
- elsif user.ldap_blocked?
+ if user.ldap_blocked?
forbidden!('LDAP blocked users cannot be unblocked by the API')
else
user.activate
end
end
- desc 'Get contribution events of a specified user' do
+ desc 'Get the contribution events of a specified user' do
detail 'This feature was introduced in GitLab 8.13.'
success Entities::Event
end
params do
- requires :id, type: String, desc: 'The user ID'
+ requires :id, type: Integer, desc: 'The ID of the user'
+ use :pagination
end
get ':id/events' do
- user = User.find_by(id: declared(params).id)
+ user = User.find_by(id: params[:id])
not_found!('User') unless user
events = user.events.
@@ -344,43 +352,43 @@ module API
end
resource :user do
- # Get currently authenticated user
- #
- # Example Request:
- # GET /user
+ desc 'Get the currently authenticated user' do
+ success Entities::UserPublic
+ end
get do
- present @current_user, with: Entities::UserFull
+ present current_user, with: sudo? ? Entities::UserWithPrivateToken : Entities::UserPublic
end
- # Get currently authenticated user's keys
- #
- # Example Request:
- # GET /user/keys
+ desc "Get the currently authenticated user's SSH keys" do
+ success Entities::SSHKey
+ end
get "keys" do
present current_user.keys, with: Entities::SSHKey
end
- # Get single key owned by currently authenticated user
- #
- # Example Request:
- # GET /user/keys/:id
- get "keys/:id" do
- key = current_user.keys.find params[:id]
+ desc 'Get a single key owned by currently authenticated user' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the SSH key'
+ end
+ get "keys/:key_id" do
+ key = current_user.keys.find_by(id: params[:key_id])
+ not_found!('Key') unless key
+
present key, with: Entities::SSHKey
end
- # Add new ssh key to currently authenticated user
- #
- # Parameters:
- # key (required) - New SSH Key
- # title (required) - New SSH Key's title
- # Example Request:
- # POST /user/keys
+ desc 'Add a new SSH key to the currently authenticated user' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :key, type: String, desc: 'The new SSH key'
+ requires :title, type: String, desc: 'The title of the new SSH key'
+ end
post "keys" do
- required_attributes! [:title, :key]
+ key = current_user.keys.new(declared_params)
- attrs = attributes_for_keys [:title, :key]
- key = current_user.keys.new attrs
if key.save
present key, with: Entities::SSHKey
else
@@ -388,48 +396,48 @@ module API
end
end
- # Delete existing ssh key of currently authenticated user
- #
- # Parameters:
- # id (required) - SSH Key ID
- # Example Request:
- # DELETE /user/keys/:id
- delete "keys/:id" do
- begin
- key = current_user.keys.find params[:id]
- key.destroy
- rescue
- end
+ desc 'Delete an SSH key from the currently authenticated user' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the SSH key'
+ end
+ delete "keys/:key_id" do
+ key = current_user.keys.find_by(id: params[:key_id])
+ not_found!('Key') unless key
+
+ present key.destroy, with: Entities::SSHKey
end
- # Get currently authenticated user's emails
- #
- # Example Request:
- # GET /user/emails
+ desc "Get the currently authenticated user's email addresses" do
+ success Entities::Email
+ end
get "emails" do
present current_user.emails, with: Entities::Email
end
- # Get single email owned by currently authenticated user
- #
- # Example Request:
- # GET /user/emails/:id
- get "emails/:id" do
- email = current_user.emails.find params[:id]
+ desc 'Get a single email address owned by the currently authenticated user' do
+ success Entities::Email
+ end
+ params do
+ requires :email_id, type: Integer, desc: 'The ID of the email'
+ end
+ get "emails/:email_id" do
+ email = current_user.emails.find_by(id: params[:email_id])
+ not_found!('Email') unless email
+
present email, with: Entities::Email
end
- # Add new email to currently authenticated user
- #
- # Parameters:
- # email (required) - Email address
- # Example Request:
- # POST /user/emails
+ desc 'Add new email address to the currently authenticated user' do
+ success Entities::Email
+ end
+ params do
+ requires :email, type: String, desc: 'The new email'
+ end
post "emails" do
- required_attributes! [:email]
+ email = current_user.emails.new(declared_params)
- attrs = attributes_for_keys [:email]
- email = current_user.emails.new attrs
if email.save
NotificationService.new.new_email(email)
present email, with: Entities::Email
@@ -438,20 +446,16 @@ module API
end
end
- # Delete existing email of currently authenticated user
- #
- # Parameters:
- # id (required) - EMail ID
- # Example Request:
- # DELETE /user/emails/:id
- delete "emails/:id" do
- begin
- email = current_user.emails.find params[:id]
- email.destroy
+ desc 'Delete an email address from the currently authenticated user'
+ params do
+ requires :email_id, type: Integer, desc: 'The ID of the email'
+ end
+ delete "emails/:email_id" do
+ email = current_user.emails.find_by(id: params[:email_id])
+ not_found!('Email') unless email
- current_user.update_secondary_emails!
- rescue
- end
+ email.destroy
+ current_user.update_secondary_emails!
end
end
end
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index b9fb3c21dbb..f623b1dfe9f 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -1,6 +1,8 @@
module API
# Projects variables API
class Variables < Grape::API
+ include PaginationParams
+
before { authenticate! }
before { authorize! :admin_build, user_project }
@@ -13,8 +15,7 @@ module API
success Entities::Variable
end
params do
- optional :page, type: Integer, desc: 'The page number for pagination'
- optional :per_page, type: Integer, desc: 'The value of items per page to show'
+ use :pagination
end
get ':id/variables' do
variables = user_project.variables
@@ -29,7 +30,7 @@ module API
end
get ':id/variables/:key' do
key = params[:key]
- variable = user_project.variables.find_by(key: key.to_s)
+ variable = user_project.variables.find_by(key: key)
return not_found!('Variable') unless variable
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 0dfffaf0bc6..7e6537e3d9e 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -14,7 +14,7 @@ module Backup
s[:gitlab_version] = Gitlab::VERSION
s[:tar_version] = tar_version
s[:skipped] = ENV["SKIP"]
- tar_file = "#{s[:backup_created_at].to_i}_gitlab_backup.tar"
+ tar_file = s[:backup_created_at].strftime('%s_%Y_%m_%d') + '_gitlab_backup.tar'
Dir.chdir(Gitlab.config.backup.path) do
File.open("#{Gitlab.config.backup.path}/backup_information.yml",
@@ -82,12 +82,17 @@ module Backup
removed = 0
Dir.chdir(Gitlab.config.backup.path) do
- file_list = Dir.glob('*_gitlab_backup.tar')
- file_list.map! { |f| $1.to_i if f =~ /(\d+)_gitlab_backup.tar/ }
- file_list.sort.each do |timestamp|
+ Dir.glob('*_gitlab_backup.tar').each do |file|
+ next unless file =~ /(\d+)(?:_\d{4}_\d{2}_\d{2})?_gitlab_backup\.tar/
+
+ timestamp = $1.to_i
+
if Time.at(timestamp) < (Time.now - keep_time)
- if Kernel.system(*%W(rm #{timestamp}_gitlab_backup.tar))
+ begin
+ FileUtils.rm(file)
removed += 1
+ rescue => e
+ $progress.puts "Deleting #{file} failed: #{e.message}".color(:red)
end
end
end
@@ -103,7 +108,7 @@ module Backup
Dir.chdir(Gitlab.config.backup.path)
# check for existing backups in the backup dir
- file_list = Dir.glob("*_gitlab_backup.tar").each.map { |f| f.split(/_/).first.to_i }
+ file_list = Dir.glob("*_gitlab_backup.tar")
puts "no backups found" if file_list.count == 0
if file_list.count > 1 && ENV["BACKUP"].nil?
@@ -112,7 +117,7 @@ module Backup
exit 1
end
- tar_file = ENV["BACKUP"].nil? ? File.join("#{file_list.first}_gitlab_backup.tar") : File.join(ENV["BACKUP"] + "_gitlab_backup.tar")
+ tar_file = ENV["BACKUP"].nil? ? file_list.first : file_list.grep(ENV['BACKUP']).first
unless File.exist?(tar_file)
puts "The specified backup doesn't exist!"
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index 3740d4fb4cd..fd74eeaebe7 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -33,7 +33,7 @@ module Banzai
# Returns a String replaced with the return of the block.
def self.references_in(text, pattern = object_class.reference_pattern)
text.gsub(pattern) do |match|
- yield match, $~[object_sym].to_i, $~[:project], $~
+ yield match, $~[object_sym].to_i, $~[:project], $~[:namespace], $~
end
end
@@ -145,8 +145,9 @@ module Banzai
# Returns a String with references replaced with links. All links
# have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
def object_link_filter(text, pattern, link_content: nil)
- references_in(text, pattern) do |match, id, project_ref, matches|
- project = project_from_ref_cached(project_ref)
+ references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches|
+ project_path = full_project_path(namespace_ref, project_ref)
+ project = project_from_ref_cached(project_path)
if project && object = find_object_cached(project, id)
title = object_link_title(object)
@@ -217,10 +218,9 @@ module Banzai
nodes.each do |node|
node.to_html.scan(regex) do
- project = $~[:project] || current_project_path
+ project_path = full_project_path($~[:namespace], $~[:project])
symbol = $~[object_sym]
-
- refs[project] << symbol if object_class.reference_valid?(symbol)
+ refs[project_path] << symbol if object_class.reference_valid?(symbol)
end
end
@@ -248,7 +248,7 @@ module Banzai
end
def projects_relation_for_paths(paths)
- Project.where_paths_in(paths).includes(:namespace)
+ Project.where_full_path_in(paths).includes(:namespace)
end
# Returns projects for the given paths.
@@ -272,8 +272,19 @@ module Banzai
@current_project_path ||= project.path_with_namespace
end
+ def current_project_namespace_path
+ @current_project_namespace_path ||= project.namespace.path
+ end
+
private
+ def full_project_path(namespace, project_ref)
+ return current_project_path unless project_ref
+
+ namespace_ref = namespace || current_project_namespace_path
+ "#{namespace_ref}/#{project_ref}"
+ end
+
def project_refs_cache
RequestStore[:banzai_project_refs] ||= {}
end
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index 799b83b1069..80c844baecd 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -71,6 +71,14 @@ module Banzai
@doc = parse_html(rinku)
end
+ # Return true if any of the UNSAFE_PROTOCOLS strings are included in the URI scheme
+ def contains_unsafe?(scheme)
+ return false unless scheme
+
+ scheme = scheme.strip.downcase
+ Banzai::Filter::SanitizationFilter::UNSAFE_PROTOCOLS.any? { |protocol| scheme.include?(protocol) }
+ end
+
# Autolinks any text matching LINK_PATTERN that Rinku didn't already
# replace
def text_parse
@@ -89,17 +97,27 @@ module Banzai
doc
end
- def autolink_filter(text)
- text.gsub(LINK_PATTERN) do |match|
- # Remove any trailing HTML entities and store them for appending
- # outside the link element. The entity must be marked HTML safe in
- # order to be output literally rather than escaped.
- match.gsub!(/((?:&[\w#]+;)+)\z/, '')
- dropped = ($1 || '').html_safe
-
- options = link_options.merge(href: match)
- content_tag(:a, match, options) + dropped
+ def autolink_match(match)
+ # start by stripping out dangerous links
+ begin
+ uri = Addressable::URI.parse(match)
+ return match if contains_unsafe?(uri.scheme)
+ rescue Addressable::URI::InvalidURIError
+ return match
end
+
+ # Remove any trailing HTML entities and store them for appending
+ # outside the link element. The entity must be marked HTML safe in
+ # order to be output literally rather than escaped.
+ match.gsub!(/((?:&[\w#]+;)+)\z/, '')
+ dropped = ($1 || '').html_safe
+
+ options = link_options.merge(href: match)
+ content_tag(:a, match, options) + dropped
+ end
+
+ def autolink_filter(text)
+ text.gsub(LINK_PATTERN) { |match| autolink_match(match) }
end
def link_options
diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb
index 4358bf45549..eaacb9591b1 100644
--- a/lib/banzai/filter/commit_range_reference_filter.rb
+++ b/lib/banzai/filter/commit_range_reference_filter.rb
@@ -12,7 +12,7 @@ module Banzai
def self.references_in(text, pattern = CommitRange.reference_pattern)
text.gsub(pattern) do |match|
- yield match, $~[:commit_range], $~[:project], $~
+ yield match, $~[:commit_range], $~[:project], $~[:namespace], $~
end
end
diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb
index a26dd09c25a..69c06117eda 100644
--- a/lib/banzai/filter/commit_reference_filter.rb
+++ b/lib/banzai/filter/commit_reference_filter.rb
@@ -12,7 +12,7 @@ module Banzai
def self.references_in(text, pattern = Commit.reference_pattern)
text.gsub(pattern) do |match|
- yield match, $~[:commit], $~[:project], $~
+ yield match, $~[:commit], $~[:project], $~[:namespace], $~
end
end
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
index 9f9a96cdc65..a605dea149e 100644
--- a/lib/banzai/filter/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -14,16 +14,18 @@ module Banzai
def self.references_in(text, pattern = Label.reference_pattern)
unescape_html_entities(text).gsub(pattern) do |match|
- yield match, $~[:label_id].to_i, $~[:label_name], $~[:project], $~
+ yield match, $~[:label_id].to_i, $~[:label_name], $~[:project], $~[:namespace], $~
end
end
def references_in(text, pattern = Label.reference_pattern)
unescape_html_entities(text).gsub(pattern) do |match|
- label = find_label($~[:project], $~[:label_id], $~[:label_name])
+ namespace, project = $~[:namespace], $~[:project]
+ project_path = full_project_path(namespace, project)
+ label = find_label(project_path, $~[:label_id], $~[:label_name])
if label
- yield match, label.id, $~[:project], $~
+ yield match, label.id, project, namespace, $~
else
match
end
@@ -64,48 +66,12 @@ module Banzai
end
def object_link_text(object, matches)
- if same_group?(object) && namespace_match?(matches)
- render_same_project_label(object)
- elsif same_project?(object)
- render_same_project_label(object)
- else
- render_cross_project_label(object, matches)
- end
- end
-
- def same_group?(object)
- object.is_a?(GroupLabel) && object.group == project.group
- end
-
- def namespace_match?(matches)
- matches[:project].blank? || matches[:project] == project.path_with_namespace
- end
-
- def same_project?(object)
- object.is_a?(ProjectLabel) && object.project == project
- end
-
- def user
- context[:current_user] || context[:author]
- end
-
- def project
- context[:project]
- end
-
- def render_same_project_label(object)
- LabelsHelper.render_colored_label(object)
- end
-
- def render_cross_project_label(object, matches)
- source_project =
- if matches[:project]
- Project.find_with_namespace(matches[:project])
- else
- object.project
- end
+ project_path = full_project_path(matches[:namespace], matches[:project])
+ project_from_ref = project_from_ref_cached(project_path)
+ reference = project_from_ref.to_human_reference(project)
+ label_suffix = " <i>in #{reference}</i>" if reference.present?
- LabelsHelper.render_colored_cross_project_label(object, source_project)
+ LabelsHelper.render_colored_label(object, label_suffix)
end
def unescape_html_entities(text)
diff --git a/lib/banzai/filter/math_filter.rb b/lib/banzai/filter/math_filter.rb
new file mode 100644
index 00000000000..cb037f89337
--- /dev/null
+++ b/lib/banzai/filter/math_filter.rb
@@ -0,0 +1,51 @@
+require 'uri'
+
+module Banzai
+ module Filter
+ # HTML filter that adds class="code math" and removes the dollar sign in $`2+2`$.
+ #
+ class MathFilter < HTML::Pipeline::Filter
+ # This picks out <code>...</code>.
+ INLINE_MATH = 'descendant-or-self::code'.freeze
+
+ # Pick out a code block which is declared math
+ DISPLAY_MATH = "descendant-or-self::pre[contains(@class, 'math') and contains(@class, 'code')]".freeze
+
+ # Attribute indicating inline or display math.
+ STYLE_ATTRIBUTE = 'data-math-style'.freeze
+
+ # Class used for tagging elements that should be rendered
+ TAG_CLASS = 'js-render-math'.freeze
+
+ INLINE_CLASSES = "code math #{TAG_CLASS}".freeze
+
+ DOLLAR_SIGN = '$'.freeze
+
+ def call
+ doc.xpath(INLINE_MATH).each do |code|
+ closing = code.next
+ opening = code.previous
+
+ # We need a sibling before and after.
+ # They should end and start with $ respectively.
+ if closing && opening &&
+ closing.content.first == DOLLAR_SIGN &&
+ opening.content.last == DOLLAR_SIGN
+
+ code[:class] = INLINE_CLASSES
+ code[STYLE_ATTRIBUTE] = 'inline'
+ closing.content = closing.content[1..-1]
+ opening.content = opening.content[0..-2]
+ end
+ end
+
+ doc.xpath(DISPLAY_MATH).each do |el|
+ el[STYLE_ATTRIBUTE] = 'display'
+ el[:class] += " #{TAG_CLASS}"
+ end
+
+ doc
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
index 58fff496d00..f12014e191f 100644
--- a/lib/banzai/filter/milestone_reference_filter.rb
+++ b/lib/banzai/filter/milestone_reference_filter.rb
@@ -19,18 +19,20 @@ module Banzai
return super(text, pattern) if pattern != Milestone.reference_pattern
text.gsub(pattern) do |match|
- milestone = find_milestone($~[:project], $~[:milestone_iid], $~[:milestone_name])
+ milestone = find_milestone($~[:project], $~[:namespace], $~[:milestone_iid], $~[:milestone_name])
if milestone
- yield match, milestone.iid, $~[:project], $~
+ yield match, milestone.iid, $~[:project], $~[:namespace], $~
else
match
end
end
end
- def find_milestone(project_ref, milestone_id, milestone_name)
- project = project_from_ref(project_ref)
+ def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name)
+ project_path = full_project_path(namespace_ref, project_ref)
+ project = project_from_ref(project_path)
+
return unless project
milestone_params = milestone_params(milestone_id, milestone_name)
@@ -52,11 +54,13 @@ module Banzai
end
def object_link_text(object, matches)
- if context[:project] == object.project
- super
+ milestone_link = escape_once(super)
+ reference = object.project.to_reference(project)
+
+ if reference.present?
+ "#{milestone_link} <i>in #{reference}</i>".html_safe
else
- "#{escape_once(super)} <i>in #{escape_once(object.project.path_with_namespace)}</i>".
- html_safe
+ milestone_link
end
end
diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb
index f09d78be0ce..9e23c8f8c55 100644
--- a/lib/banzai/filter/relative_link_filter.rb
+++ b/lib/banzai/filter/relative_link_filter.rb
@@ -46,7 +46,7 @@ module Banzai
end
def rebuild_relative_uri(uri)
- file_path = relative_file_path(uri.path)
+ file_path = relative_file_path(uri)
uri.path = [
relative_url_root,
@@ -59,8 +59,10 @@ module Banzai
uri
end
- def relative_file_path(path)
- nested_path = build_relative_path(path, context[:requested_path])
+ def relative_file_path(uri)
+ path = Addressable::URI.unescape(uri.path)
+ request_path = Addressable::URI.unescape(context[:requested_path])
+ nested_path = build_relative_path(path, request_path)
file_exists?(nested_path) ? nested_path : path
end
@@ -108,11 +110,7 @@ module Banzai
end
def uri_type(path)
- @uri_types[path] ||= begin
- unescaped_path = Addressable::URI.unescape(path)
-
- current_commit.uri_type(unescaped_path)
- end
+ @uri_types[path] ||= current_commit.uri_type(path)
end
def current_commit
diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb
index a4eda6fdf76..8e7084f2543 100644
--- a/lib/banzai/filter/table_of_contents_filter.rb
+++ b/lib/banzai/filter/table_of_contents_filter.rb
@@ -35,9 +35,11 @@ module Banzai
headers[id] += 1
if header_content = node.children.first
+ # namespace detection will be automatically handled via javascript (see issue #22781)
+ namespace = "user-content-"
href = "#{id}#{uniq}"
push_toc(href, text)
- header_content.add_previous_sibling(anchor_tag(href))
+ header_content.add_previous_sibling(anchor_tag("#{namespace}#{href}", href))
end
end
@@ -48,8 +50,8 @@ module Banzai
private
- def anchor_tag(href)
- %Q{<a id="#{href}" class="anchor" href="##{href}" aria-hidden="true"></a>}
+ def anchor_tag(id, href)
+ %Q{<a id="#{id}" class="anchor" href="##{href}" aria-hidden="true"></a>}
end
def push_toc(href, text)
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 5da2d0b008c..5a1f873496c 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -6,6 +6,7 @@ module Banzai
Filter::SyntaxHighlightFilter,
Filter::SanitizationFilter,
+ Filter::MathFilter,
Filter::UploadLinkFilter,
Filter::VideoLinkFilter,
Filter::ImageLinkFilter,
diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb
index f5d110e987b..d8a855ec1fe 100644
--- a/lib/banzai/reference_parser/base_parser.rb
+++ b/lib/banzai/reference_parser/base_parser.rb
@@ -63,12 +63,7 @@ module Banzai
nodes.select do |node|
if node.has_attribute?(project_attr)
node_id = node.attr(project_attr).to_i
-
- if project && project.id == node_id
- true
- else
- can?(user, :read_project, projects[node_id])
- end
+ can_read_reference?(user, projects[node_id])
else
true
end
@@ -226,6 +221,15 @@ module Banzai
attr_reader :current_user, :project
+ # When a feature is disabled or visible only for
+ # team members we should not allow team members
+ # see reference comments.
+ # Override this method on subclasses
+ # to check if user can read resource
+ def can_read_reference?(user, ref_project)
+ raise NotImplementedError
+ end
+
def lazy(&block)
Gitlab::Lazy.new(&block)
end
diff --git a/lib/banzai/reference_parser/commit_parser.rb b/lib/banzai/reference_parser/commit_parser.rb
index 0fee9d267de..8c54a041cb8 100644
--- a/lib/banzai/reference_parser/commit_parser.rb
+++ b/lib/banzai/reference_parser/commit_parser.rb
@@ -29,6 +29,12 @@ module Banzai
commits
end
+
+ private
+
+ def can_read_reference?(user, ref_project)
+ can?(user, :download_code, ref_project)
+ end
end
end
end
diff --git a/lib/banzai/reference_parser/commit_range_parser.rb b/lib/banzai/reference_parser/commit_range_parser.rb
index 69d01f8db15..0878b6afba3 100644
--- a/lib/banzai/reference_parser/commit_range_parser.rb
+++ b/lib/banzai/reference_parser/commit_range_parser.rb
@@ -33,6 +33,12 @@ module Banzai
range.valid_commits? ? range : nil
end
+
+ private
+
+ def can_read_reference?(user, ref_project)
+ can?(user, :download_code, ref_project)
+ end
end
end
end
diff --git a/lib/banzai/reference_parser/external_issue_parser.rb b/lib/banzai/reference_parser/external_issue_parser.rb
index a1264db2111..6e7b7669578 100644
--- a/lib/banzai/reference_parser/external_issue_parser.rb
+++ b/lib/banzai/reference_parser/external_issue_parser.rb
@@ -20,6 +20,12 @@ module Banzai
def issue_ids_per_project(nodes)
gather_attributes_per_project(nodes, self.class.data_attribute)
end
+
+ private
+
+ def can_read_reference?(user, ref_project)
+ can?(user, :read_issue, ref_project)
+ end
end
end
end
diff --git a/lib/banzai/reference_parser/label_parser.rb b/lib/banzai/reference_parser/label_parser.rb
index e5d1eb11d7f..aa76c64ac5f 100644
--- a/lib/banzai/reference_parser/label_parser.rb
+++ b/lib/banzai/reference_parser/label_parser.rb
@@ -6,6 +6,12 @@ module Banzai
def references_relation
Label
end
+
+ private
+
+ def can_read_reference?(user, ref_project)
+ can?(user, :read_label, ref_project)
+ end
end
end
end
diff --git a/lib/banzai/reference_parser/merge_request_parser.rb b/lib/banzai/reference_parser/merge_request_parser.rb
index c9a9ca79c09..40451947e6c 100644
--- a/lib/banzai/reference_parser/merge_request_parser.rb
+++ b/lib/banzai/reference_parser/merge_request_parser.rb
@@ -6,6 +6,12 @@ module Banzai
def references_relation
MergeRequest.includes(:author, :assignee, :target_project)
end
+
+ private
+
+ def can_read_reference?(user, ref_project)
+ can?(user, :read_merge_request, ref_project)
+ end
end
end
end
diff --git a/lib/banzai/reference_parser/milestone_parser.rb b/lib/banzai/reference_parser/milestone_parser.rb
index a000ac61e5c..d3968d6b229 100644
--- a/lib/banzai/reference_parser/milestone_parser.rb
+++ b/lib/banzai/reference_parser/milestone_parser.rb
@@ -6,6 +6,12 @@ module Banzai
def references_relation
Milestone
end
+
+ private
+
+ def can_read_reference?(user, ref_project)
+ can?(user, :read_milestone, ref_project)
+ end
end
end
end
diff --git a/lib/banzai/reference_parser/snippet_parser.rb b/lib/banzai/reference_parser/snippet_parser.rb
index fa71b3c952a..63b592137bb 100644
--- a/lib/banzai/reference_parser/snippet_parser.rb
+++ b/lib/banzai/reference_parser/snippet_parser.rb
@@ -6,6 +6,12 @@ module Banzai
def references_relation
Snippet
end
+
+ private
+
+ def can_read_reference?(user, ref_project)
+ can?(user, :read_project_snippet, ref_project)
+ end
end
end
end
diff --git a/lib/banzai/reference_parser/user_parser.rb b/lib/banzai/reference_parser/user_parser.rb
index 863f5725d3b..7adaffa19c1 100644
--- a/lib/banzai/reference_parser/user_parser.rb
+++ b/lib/banzai/reference_parser/user_parser.rb
@@ -30,22 +30,36 @@ module Banzai
nodes.each do |node|
if node.has_attribute?(group_attr)
- node_group = groups[node.attr(group_attr).to_i]
-
- if node_group &&
- can?(user, :read_group, node_group)
- visible << node
- end
- # Remaining nodes will be processed by the parent class'
- # implementation of this method.
+ next unless can_read_group_reference?(node, user, groups)
+ visible << node
+ elsif can_read_project_reference?(node)
+ visible << node
else
remaining << node
end
end
+ # If project does not belong to a group
+ # and does not have the same project id as the current project
+ # base class will check if user can read the project that contains
+ # the user reference.
visible + super(current_user, remaining)
end
+ # Check if project belongs to a group which
+ # user can read.
+ def can_read_group_reference?(node, user, groups)
+ node_group = groups[node.attr('data-group').to_i]
+
+ node_group && can?(user, :read_group, node_group)
+ end
+
+ def can_read_project_reference?(node)
+ node_id = node.attr('data-project').to_i
+
+ project && project.id == node_id
+ end
+
def nodes_user_can_reference(current_user, nodes)
project_attr = 'data-project'
author_attr = 'data-author'
@@ -88,6 +102,10 @@ module Banzai
collection_objects_for_ids(Project, ids).
flat_map { |p| p.team.members.to_a }
end
+
+ def can_read_reference?(user, ref_project)
+ can?(user, :read_project, ref_project)
+ end
end
end
end
diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb
new file mode 100644
index 00000000000..f8ee7e0f9ae
--- /dev/null
+++ b/lib/bitbucket/client.rb
@@ -0,0 +1,58 @@
+module Bitbucket
+ class Client
+ attr_reader :connection
+
+ def initialize(options = {})
+ @connection = Connection.new(options)
+ end
+
+ def issues(repo)
+ path = "/repositories/#{repo}/issues"
+ get_collection(path, :issue)
+ end
+
+ def issue_comments(repo, issue_id)
+ path = "/repositories/#{repo}/issues/#{issue_id}/comments"
+ get_collection(path, :comment)
+ end
+
+ def pull_requests(repo)
+ path = "/repositories/#{repo}/pullrequests?state=ALL"
+ get_collection(path, :pull_request)
+ end
+
+ def pull_request_comments(repo, pull_request)
+ path = "/repositories/#{repo}/pullrequests/#{pull_request}/comments"
+ get_collection(path, :pull_request_comment)
+ end
+
+ def pull_request_diff(repo, pull_request)
+ path = "/repositories/#{repo}/pullrequests/#{pull_request}/diff"
+ connection.get(path)
+ end
+
+ def repo(name)
+ parsed_response = connection.get("/repositories/#{name}")
+ Representation::Repo.new(parsed_response)
+ end
+
+ def repos
+ path = "/repositories?role=member"
+ get_collection(path, :repo)
+ end
+
+ def user
+ @user ||= begin
+ parsed_response = connection.get('/user')
+ Representation::User.new(parsed_response)
+ end
+ end
+
+ private
+
+ def get_collection(path, type)
+ paginator = Paginator.new(connection, path, type)
+ Collection.new(paginator)
+ end
+ end
+end
diff --git a/lib/bitbucket/collection.rb b/lib/bitbucket/collection.rb
new file mode 100644
index 00000000000..3a9379ff680
--- /dev/null
+++ b/lib/bitbucket/collection.rb
@@ -0,0 +1,21 @@
+module Bitbucket
+ class Collection < Enumerator
+ def initialize(paginator)
+ super() do |yielder|
+ loop do
+ paginator.items.each { |item| yielder << item }
+ end
+ end
+
+ lazy
+ end
+
+ def method_missing(method, *args)
+ return super unless self.respond_to?(method)
+
+ self.send(method, *args) do |item|
+ block_given? ? yield(item) : item
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb
new file mode 100644
index 00000000000..7e55cf4deab
--- /dev/null
+++ b/lib/bitbucket/connection.rb
@@ -0,0 +1,69 @@
+module Bitbucket
+ class Connection
+ DEFAULT_API_VERSION = '2.0'
+ DEFAULT_BASE_URI = 'https://api.bitbucket.org/'
+ DEFAULT_QUERY = {}
+
+ attr_reader :expires_at, :expires_in, :refresh_token, :token
+
+ def initialize(options = {})
+ @api_version = options.fetch(:api_version, DEFAULT_API_VERSION)
+ @base_uri = options.fetch(:base_uri, DEFAULT_BASE_URI)
+ @default_query = options.fetch(:query, DEFAULT_QUERY)
+
+ @token = options[:token]
+ @expires_at = options[:expires_at]
+ @expires_in = options[:expires_in]
+ @refresh_token = options[:refresh_token]
+ end
+
+ def get(path, extra_query = {})
+ refresh! if expired?
+
+ response = connection.get(build_url(path), params: @default_query.merge(extra_query))
+ response.parsed
+ end
+
+ def expired?
+ connection.expired?
+ end
+
+ def refresh!
+ response = connection.refresh!
+
+ @token = response.token
+ @expires_at = response.expires_at
+ @expires_in = response.expires_in
+ @refresh_token = response.refresh_token
+ @connection = nil
+ end
+
+ private
+
+ def client
+ @client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options)
+ end
+
+ def connection
+ @connection ||= OAuth2::AccessToken.new(client, @token, refresh_token: @refresh_token, expires_at: @expires_at, expires_in: @expires_in)
+ end
+
+ def build_url(path)
+ return path if path.starts_with?(root_url)
+
+ "#{root_url}#{path}"
+ end
+
+ def root_url
+ @root_url ||= "#{@base_uri}#{@api_version}"
+ end
+
+ def provider
+ Gitlab::OAuth::Provider.config_for('bitbucket')
+ end
+
+ def options
+ OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys
+ end
+ end
+end
diff --git a/lib/bitbucket/error/unauthorized.rb b/lib/bitbucket/error/unauthorized.rb
new file mode 100644
index 00000000000..5e2eb57bb0e
--- /dev/null
+++ b/lib/bitbucket/error/unauthorized.rb
@@ -0,0 +1,6 @@
+module Bitbucket
+ module Error
+ class Unauthorized < StandardError
+ end
+ end
+end
diff --git a/lib/bitbucket/page.rb b/lib/bitbucket/page.rb
new file mode 100644
index 00000000000..2b0a3fe7b1a
--- /dev/null
+++ b/lib/bitbucket/page.rb
@@ -0,0 +1,34 @@
+module Bitbucket
+ class Page
+ attr_reader :attrs, :items
+
+ def initialize(raw, type)
+ @attrs = parse_attrs(raw)
+ @items = parse_values(raw, representation_class(type))
+ end
+
+ def next?
+ attrs.fetch(:next, false)
+ end
+
+ def next
+ attrs.fetch(:next)
+ end
+
+ private
+
+ def parse_attrs(raw)
+ raw.slice(*%w(size page pagelen next previous)).symbolize_keys
+ end
+
+ def parse_values(raw, bitbucket_rep_class)
+ return [] unless raw['values'] && raw['values'].is_a?(Array)
+
+ bitbucket_rep_class.decorate(raw['values'])
+ end
+
+ def representation_class(type)
+ Bitbucket::Representation.const_get(type.to_s.camelize)
+ end
+ end
+end
diff --git a/lib/bitbucket/paginator.rb b/lib/bitbucket/paginator.rb
new file mode 100644
index 00000000000..135d0d55674
--- /dev/null
+++ b/lib/bitbucket/paginator.rb
@@ -0,0 +1,36 @@
+module Bitbucket
+ class Paginator
+ PAGE_LENGTH = 50 # The minimum length is 10 and the maximum is 100.
+
+ def initialize(connection, url, type)
+ @connection = connection
+ @type = type
+ @url = url
+ @page = nil
+ end
+
+ def items
+ raise StopIteration unless has_next_page?
+
+ @page = fetch_next_page
+ @page.items
+ end
+
+ private
+
+ attr_reader :connection, :page, :url, :type
+
+ def has_next_page?
+ page.nil? || page.next?
+ end
+
+ def next_url
+ page.nil? ? url : page.next
+ end
+
+ def fetch_next_page
+ parsed_response = connection.get(next_url, pagelen: PAGE_LENGTH, sort: :created_on)
+ Page.new(parsed_response, type)
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/base.rb b/lib/bitbucket/representation/base.rb
new file mode 100644
index 00000000000..94adaacc9b5
--- /dev/null
+++ b/lib/bitbucket/representation/base.rb
@@ -0,0 +1,17 @@
+module Bitbucket
+ module Representation
+ class Base
+ def initialize(raw)
+ @raw = raw
+ end
+
+ def self.decorate(entries)
+ entries.map { |entry| new(entry)}
+ end
+
+ private
+
+ attr_reader :raw
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/comment.rb b/lib/bitbucket/representation/comment.rb
new file mode 100644
index 00000000000..4937aa9728f
--- /dev/null
+++ b/lib/bitbucket/representation/comment.rb
@@ -0,0 +1,27 @@
+module Bitbucket
+ module Representation
+ class Comment < Representation::Base
+ def author
+ user['username']
+ end
+
+ def note
+ raw.fetch('content', {}).fetch('raw', nil)
+ end
+
+ def created_at
+ raw['created_on']
+ end
+
+ def updated_at
+ raw['updated_on'] || raw['created_on']
+ end
+
+ private
+
+ def user
+ raw.fetch('user', {})
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/issue.rb b/lib/bitbucket/representation/issue.rb
new file mode 100644
index 00000000000..054064395c3
--- /dev/null
+++ b/lib/bitbucket/representation/issue.rb
@@ -0,0 +1,53 @@
+module Bitbucket
+ module Representation
+ class Issue < Representation::Base
+ CLOSED_STATUS = %w(resolved invalid duplicate wontfix closed).freeze
+
+ def iid
+ raw['id']
+ end
+
+ def kind
+ raw['kind']
+ end
+
+ def author
+ raw.fetch('reporter', {}).fetch('username', nil)
+ end
+
+ def description
+ raw.fetch('content', {}).fetch('raw', nil)
+ end
+
+ def state
+ closed? ? 'closed' : 'opened'
+ end
+
+ def title
+ raw['title']
+ end
+
+ def milestone
+ raw['milestone']['name'] if raw['milestone'].present?
+ end
+
+ def created_at
+ raw['created_on']
+ end
+
+ def updated_at
+ raw['edited_on']
+ end
+
+ def to_s
+ iid
+ end
+
+ private
+
+ def closed?
+ CLOSED_STATUS.include?(raw['state'])
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/pull_request.rb b/lib/bitbucket/representation/pull_request.rb
new file mode 100644
index 00000000000..eebf8093380
--- /dev/null
+++ b/lib/bitbucket/representation/pull_request.rb
@@ -0,0 +1,65 @@
+module Bitbucket
+ module Representation
+ class PullRequest < Representation::Base
+ def author
+ raw.fetch('author', {}).fetch('username', nil)
+ end
+
+ def description
+ raw['description']
+ end
+
+ def iid
+ raw['id']
+ end
+
+ def state
+ if raw['state'] == 'MERGED'
+ 'merged'
+ elsif raw['state'] == 'DECLINED'
+ 'closed'
+ else
+ 'opened'
+ end
+ end
+
+ def created_at
+ raw['created_on']
+ end
+
+ def updated_at
+ raw['updated_on']
+ end
+
+ def title
+ raw['title']
+ end
+
+ def source_branch_name
+ source_branch.fetch('branch', {}).fetch('name', nil)
+ end
+
+ def source_branch_sha
+ source_branch.fetch('commit', {}).fetch('hash', nil)
+ end
+
+ def target_branch_name
+ target_branch.fetch('branch', {}).fetch('name', nil)
+ end
+
+ def target_branch_sha
+ target_branch.fetch('commit', {}).fetch('hash', nil)
+ end
+
+ private
+
+ def source_branch
+ raw['source']
+ end
+
+ def target_branch
+ raw['destination']
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/pull_request_comment.rb b/lib/bitbucket/representation/pull_request_comment.rb
new file mode 100644
index 00000000000..4f8efe03bae
--- /dev/null
+++ b/lib/bitbucket/representation/pull_request_comment.rb
@@ -0,0 +1,39 @@
+module Bitbucket
+ module Representation
+ class PullRequestComment < Comment
+ def iid
+ raw['id']
+ end
+
+ def file_path
+ inline.fetch('path')
+ end
+
+ def old_pos
+ inline.fetch('from')
+ end
+
+ def new_pos
+ inline.fetch('to')
+ end
+
+ def parent_id
+ raw.fetch('parent', {}).fetch('id', nil)
+ end
+
+ def inline?
+ raw.has_key?('inline')
+ end
+
+ def has_parent?
+ raw.has_key?('parent')
+ end
+
+ private
+
+ def inline
+ raw.fetch('inline', {})
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/repo.rb b/lib/bitbucket/representation/repo.rb
new file mode 100644
index 00000000000..8969ecd1c19
--- /dev/null
+++ b/lib/bitbucket/representation/repo.rb
@@ -0,0 +1,67 @@
+module Bitbucket
+ module Representation
+ class Repo < Representation::Base
+ attr_reader :owner, :slug
+
+ def initialize(raw)
+ super(raw)
+ end
+
+ def owner_and_slug
+ @owner_and_slug ||= full_name.split('/', 2)
+ end
+
+ def owner
+ owner_and_slug.first
+ end
+
+ def slug
+ owner_and_slug.last
+ end
+
+ def clone_url(token = nil)
+ url = raw['links']['clone'].find { |link| link['name'] == 'https' }.fetch('href')
+
+ if token.present?
+ clone_url = URI::parse(url)
+ clone_url.user = "x-token-auth:#{token}"
+ clone_url.to_s
+ else
+ url
+ end
+ end
+
+ def description
+ raw['description']
+ end
+
+ def full_name
+ raw['full_name']
+ end
+
+ def issues_enabled?
+ raw['has_issues']
+ end
+
+ def name
+ raw['name']
+ end
+
+ def valid?
+ raw['scm'] == 'git'
+ end
+
+ def visibility_level
+ if raw['is_private']
+ Gitlab::VisibilityLevel::PRIVATE
+ else
+ Gitlab::VisibilityLevel::PUBLIC
+ end
+ end
+
+ def to_s
+ full_name
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/user.rb b/lib/bitbucket/representation/user.rb
new file mode 100644
index 00000000000..ba6b7667b49
--- /dev/null
+++ b/lib/bitbucket/representation/user.rb
@@ -0,0 +1,9 @@
+module Bitbucket
+ module Representation
+ class User < Representation::Base
+ def username
+ raw['username']
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb
index 66c05773b68..792ff628b09 100644
--- a/lib/ci/api/entities.rb
+++ b/lib/ci/api/entities.rb
@@ -32,6 +32,10 @@ module Ci
expose :artifacts_file, using: ArtifactFile, if: ->(build, _) { build.artifacts? }
end
+ class BuildCredentials < Grape::Entity
+ expose :type, :url, :username, :password
+ end
+
class BuildDetails < Build
expose :commands
expose :repo_url
@@ -50,6 +54,8 @@ module Ci
expose :variables
expose :depends_on_builds, using: Build
+
+ expose :credentials, using: BuildCredentials
end
class Runner < Grape::Entity
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 3e33c9399e2..fef652cb975 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -2,7 +2,7 @@ module Ci
class GitlabCiYamlProcessor
class ValidationError < StandardError; end
- include Gitlab::Ci::Config::Node::LegacyValidationHelpers
+ include Gitlab::Ci::Config::Entry::LegacyValidationHelpers
attr_reader :path, :cache, :stages, :jobs
diff --git a/lib/constraints/group_url_constrainer.rb b/lib/constraints/group_url_constrainer.rb
index ca39b1961ae..bae4db1ca4d 100644
--- a/lib/constraints/group_url_constrainer.rb
+++ b/lib/constraints/group_url_constrainer.rb
@@ -1,7 +1,17 @@
-require 'constraints/namespace_url_constrainer'
+class GroupUrlConstrainer
+ def matches?(request)
+ id = request.params[:id]
-class GroupUrlConstrainer < NamespaceUrlConstrainer
- def find_resource(id)
- Group.find_by_path(id)
+ return false unless valid?(id)
+
+ Group.find_by_full_path(id).present?
+ end
+
+ private
+
+ def valid?(id)
+ id.split('/').all? do |namespace|
+ NamespaceValidator.valid?(namespace)
+ end
end
end
diff --git a/lib/constraints/namespace_url_constrainer.rb b/lib/constraints/namespace_url_constrainer.rb
deleted file mode 100644
index 91b70143f11..00000000000
--- a/lib/constraints/namespace_url_constrainer.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-class NamespaceUrlConstrainer
- def matches?(request)
- id = request.path
- id = id.sub(/\A#{relative_url_root}/, '') if relative_url_root
- id = id.sub(/\A\/+/, '').split('/').first
- id = id.sub(/.atom\z/, '') if id
-
- if id =~ Gitlab::Regex.namespace_regex
- find_resource(id)
- end
- end
-
- def find_resource(id)
- Namespace.find_by_path(id)
- end
-
- private
-
- def relative_url_root
- if defined?(Gitlab::Application.config.relative_url_root)
- Gitlab::Application.config.relative_url_root
- end
- end
-end
diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb
new file mode 100644
index 00000000000..730b05bed97
--- /dev/null
+++ b/lib/constraints/project_url_constrainer.rb
@@ -0,0 +1,13 @@
+class ProjectUrlConstrainer
+ def matches?(request)
+ namespace_path = request.params[:namespace_id]
+ project_path = request.params[:project_id] || request.params[:id]
+ full_path = namespace_path + '/' + project_path
+
+ unless ProjectPathValidator.valid?(project_path)
+ return false
+ end
+
+ Project.find_with_namespace(full_path).present?
+ end
+end
diff --git a/lib/constraints/user_url_constrainer.rb b/lib/constraints/user_url_constrainer.rb
index 504a0f5d93e..9ab5bcb12ff 100644
--- a/lib/constraints/user_url_constrainer.rb
+++ b/lib/constraints/user_url_constrainer.rb
@@ -1,7 +1,5 @@
-require 'constraints/namespace_url_constrainer'
-
-class UserUrlConstrainer < NamespaceUrlConstrainer
- def find_resource(id)
- User.find_by('lower(username) = ?', id.downcase)
+class UserUrlConstrainer
+ def matches?(request)
+ User.find_by_username(request.params[:username]).present?
end
end
diff --git a/lib/email_template_interceptor.rb b/lib/email_template_interceptor.rb
new file mode 100644
index 00000000000..fb04a7824b8
--- /dev/null
+++ b/lib/email_template_interceptor.rb
@@ -0,0 +1,13 @@
+# Read about interceptors in http://guides.rubyonrails.org/action_mailer_basics.html#intercepting-emails
+class EmailTemplateInterceptor
+ include Gitlab::CurrentSettings
+
+ def self.delivering_email(message)
+ # Remove HTML part if HTML emails are disabled.
+ unless current_application_settings.html_emails_enabled
+ message.part.delete_if do |part|
+ part.content_type.try(:start_with?, 'text/html')
+ end
+ end
+ end
+end
diff --git a/lib/event_filter.rb b/lib/event_filter.rb
index 21f6a9a762b..515095af1c2 100644
--- a/lib/event_filter.rb
+++ b/lib/event_filter.rb
@@ -14,6 +14,10 @@ class EventFilter
'merged'
end
+ def issue
+ 'issue'
+ end
+
def comments
'comments'
end
@@ -32,32 +36,20 @@ class EventFilter
end
def apply_filter(events)
- return events unless params.present?
-
- filter = params.dup
- actions = []
+ return events if params.blank? || params == EventFilter.all
- case filter
+ case params
when EventFilter.push
- actions = [Event::PUSHED]
+ events.where(action: Event::PUSHED)
when EventFilter.merged
- actions = [Event::MERGED]
+ events.where(action: Event::MERGED)
when EventFilter.comments
- actions = [Event::COMMENTED]
+ events.where(action: Event::COMMENTED)
when EventFilter.team
- actions = [Event::JOINED, Event::LEFT, Event::EXPIRED]
- when EventFilter.all
- actions = [
- Event::PUSHED,
- Event::MERGED,
- Event::COMMENTED,
- Event::JOINED,
- Event::LEFT,
- Event::EXPIRED
- ]
+ events.where(action: [Event::JOINED, Event::LEFT, Event::EXPIRED])
+ when EventFilter.issue
+ events.where(action: [Event::CREATED, Event::UPDATED, Event::CLOSED, Event::REOPENED])
end
-
- events.where(action: actions)
end
def options(key)
@@ -73,6 +65,10 @@ class EventFilter
end
def active?(key)
- params.include? key
+ if params.present?
+ params.include? key
+ else
+ key == EventFilter.all
+ end
end
end
diff --git a/lib/gitlab/allowable.rb b/lib/gitlab/allowable.rb
new file mode 100644
index 00000000000..f48abcc86d5
--- /dev/null
+++ b/lib/gitlab/allowable.rb
@@ -0,0 +1,7 @@
+module Gitlab
+ module Allowable
+ def can?(user, action, subject)
+ Ability.allowed?(user, action, subject)
+ end
+ end
+end
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index 1a22ad9acf5..fa234284361 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -1,4 +1,5 @@
require 'asciidoctor'
+require 'asciidoctor/converter/html5'
module Gitlab
# Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters
@@ -6,7 +7,7 @@ module Gitlab
module Asciidoc
DEFAULT_ADOC_ATTRS = [
'showtitle', 'idprefix=user-content-', 'idseparator=-', 'env=gitlab',
- 'env-gitlab', 'source-highlighter=html-pipeline'
+ 'env-gitlab', 'source-highlighter=html-pipeline', 'icons=font'
].freeze
# Public: Converts the provided Asciidoc markup into HTML.
@@ -23,7 +24,7 @@ module Gitlab
def self.render(input, context, asciidoc_opts = {})
asciidoc_opts.reverse_merge!(
safe: :secure,
- backend: :html5,
+ backend: :gitlab_html5,
attributes: []
)
asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS)
@@ -34,5 +35,29 @@ module Gitlab
html.html_safe
end
+
+ class Html5Converter < Asciidoctor::Converter::Html5Converter
+ extend Asciidoctor::Converter::Config
+
+ register_for 'gitlab_html5'
+
+ def stem(node)
+ return super unless node.style.to_sym == :latexmath
+
+ %(<pre#{id_attribute(node)} class="code math js-render-math #{node.role}" data-math-style="display"><code>#{node.content}</code></pre>)
+ end
+
+ def inline_quoted(node)
+ return super unless node.type.to_sym == :latexmath
+
+ %(<code#{id_attribute(node)} class="code math js-render-math #{node.role}" data-math-style="inline">#{node.text}</code>)
+ end
+
+ private
+
+ def id_attribute(node)
+ node.id ? %( id="#{node.id}") : nil
+ end
+ end
end
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index aca5d0020cf..8dda65c71ef 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -2,6 +2,10 @@ module Gitlab
module Auth
class MissingPersonalTokenError < StandardError; end
+ SCOPES = [:api, :read_user]
+ DEFAULT_SCOPES = [:api]
+ OPTIONAL_SCOPES = SCOPES - DEFAULT_SCOPES
+
class << self
def find_for_git_client(login, password, project:, ip:)
raise "Must provide an IP for rate limiting" if ip.nil?
@@ -88,7 +92,7 @@ module Gitlab
def oauth_access_token_check(login, password)
if login == "oauth2" && password.present?
token = Doorkeeper::AccessToken.by_token(password)
- if token && token.accessible?
+ if valid_oauth_token?(token)
user = User.find_by(id: token.resource_owner_id)
Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)
end
@@ -97,12 +101,27 @@ module Gitlab
def personal_access_token_check(login, password)
if login && password
- user = User.find_by_personal_access_token(password)
+ token = PersonalAccessToken.active.find_by_token(password)
validation = User.by_login(login)
- Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities) if user.present? && user == validation
+
+ if valid_personal_access_token?(token, validation)
+ Gitlab::Auth::Result.new(validation, nil, :personal_token, full_authentication_abilities)
+ end
end
end
+ def valid_oauth_token?(token)
+ token && token.accessible? && valid_api_token?(token)
+ end
+
+ def valid_personal_access_token?(token, user)
+ token && token.user == user && valid_api_token?(token)
+ end
+
+ def valid_api_token?(token)
+ AccessTokenValidationService.new(token).include_any_scope?(['api'])
+ end
+
def lfs_token_check(login, password)
deploy_key_matches = login.match(/\Alfs\+deploy-key-(\d+)\z/)
diff --git a/lib/gitlab/badge/build/status.rb b/lib/gitlab/badge/build/status.rb
index 50aa45e5406..b762d85b6e5 100644
--- a/lib/gitlab/badge/build/status.rb
+++ b/lib/gitlab/badge/build/status.rb
@@ -20,8 +20,8 @@ module Gitlab
def status
@project.pipelines
- .where(sha: @sha, ref: @ref)
- .status || 'unknown'
+ .where(sha: @sha)
+ .latest_status(@ref) || 'unknown'
end
def metadata
diff --git a/lib/gitlab/bitbucket_import.rb b/lib/gitlab/bitbucket_import.rb
deleted file mode 100644
index 7298152e7e9..00000000000
--- a/lib/gitlab/bitbucket_import.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-module Gitlab
- module BitbucketImport
- mattr_accessor :public_key
- @public_key = nil
- end
-end
diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb
deleted file mode 100644
index 8d1ad62fae0..00000000000
--- a/lib/gitlab/bitbucket_import/client.rb
+++ /dev/null
@@ -1,142 +0,0 @@
-module Gitlab
- module BitbucketImport
- class Client
- class Unauthorized < StandardError; end
-
- attr_reader :consumer, :api
-
- def self.from_project(project)
- import_data_credentials = project.import_data.credentials if project.import_data
- if import_data_credentials && import_data_credentials[:bb_session]
- token = import_data_credentials[:bb_session][:bitbucket_access_token]
- token_secret = import_data_credentials[:bb_session][:bitbucket_access_token_secret]
- new(token, token_secret)
- else
- raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{project.id}"
- end
- end
-
- def initialize(access_token = nil, access_token_secret = nil)
- @consumer = ::OAuth::Consumer.new(
- config.app_id,
- config.app_secret,
- bitbucket_options
- )
-
- if access_token && access_token_secret
- @api = ::OAuth::AccessToken.new(@consumer, access_token, access_token_secret)
- end
- end
-
- def request_token(redirect_uri)
- request_token = consumer.get_request_token(oauth_callback: redirect_uri)
-
- {
- oauth_token: request_token.token,
- oauth_token_secret: request_token.secret,
- oauth_callback_confirmed: request_token.callback_confirmed?.to_s
- }
- end
-
- def authorize_url(request_token, redirect_uri)
- request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash)
-
- if request_token.callback_confirmed?
- request_token.authorize_url
- else
- request_token.authorize_url(oauth_callback: redirect_uri)
- end
- end
-
- def get_token(request_token, oauth_verifier, redirect_uri)
- request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash)
-
- if request_token.callback_confirmed?
- request_token.get_access_token(oauth_verifier: oauth_verifier)
- else
- request_token.get_access_token(oauth_callback: redirect_uri)
- end
- end
-
- def user
- JSON.parse(get("/api/1.0/user").body)
- end
-
- def issues(project_identifier)
- all_issues = []
- offset = 0
- per_page = 50 # Maximum number allowed by Bitbucket
- index = 0
-
- begin
- issues = JSON.parse(get(issue_api_endpoint(project_identifier, per_page, offset)).body)
- # Find out how many total issues are present
- total = issues["count"] if index == 0
- all_issues.concat(issues["issues"])
- offset += issues["issues"].count
- index += 1
- end while all_issues.count < total
-
- all_issues
- end
-
- def issue_comments(project_identifier, issue_id)
- comments = JSON.parse(get("/api/1.0/repositories/#{project_identifier}/issues/#{issue_id}/comments").body)
- comments.sort_by { |comment| comment["utc_created_on"] }
- end
-
- def project(project_identifier)
- JSON.parse(get("/api/1.0/repositories/#{project_identifier}").body)
- end
-
- def find_deploy_key(project_identifier, key)
- JSON.parse(get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key|
- deploy_key["key"].chomp == key.chomp
- end
- end
-
- def add_deploy_key(project_identifier, key)
- deploy_key = find_deploy_key(project_identifier, key)
- return if deploy_key
-
- JSON.parse(api.post("/api/1.0/repositories/#{project_identifier}/deploy-keys", key: key, label: "GitLab import key").body)
- end
-
- def delete_deploy_key(project_identifier, key)
- deploy_key = find_deploy_key(project_identifier, key)
- return unless deploy_key
-
- api.delete("/api/1.0/repositories/#{project_identifier}/deploy-keys/#{deploy_key["pk"]}").code == "204"
- end
-
- def projects
- JSON.parse(get("/api/1.0/user/repositories").body).select { |repo| repo["scm"] == "git" }
- end
-
- def incompatible_projects
- JSON.parse(get("/api/1.0/user/repositories").body).reject { |repo| repo["scm"] == "git" }
- end
-
- private
-
- def get(url)
- response = api.get(url)
- raise Unauthorized if (400..499).cover?(response.code.to_i)
-
- response
- end
-
- def issue_api_endpoint(project_identifier, per_page, offset)
- "/api/1.0/repositories/#{project_identifier}/issues?sort=utc_created_on&limit=#{per_page}&start=#{offset}"
- end
-
- def config
- Gitlab.config.omniauth.providers.find { |provider| provider.name == "bitbucket" }
- end
-
- def bitbucket_options
- OmniAuth::Strategies::Bitbucket.default_options[:client_options].symbolize_keys
- end
- end
- end
-end
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index f4b5097adb1..7d2f92d577a 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -1,84 +1,234 @@
module Gitlab
module BitbucketImport
class Importer
- attr_reader :project, :client
+ LABELS = [{ title: 'bug', color: '#FF0000' },
+ { title: 'enhancement', color: '#428BCA' },
+ { title: 'proposal', color: '#69D100' },
+ { title: 'task', color: '#7F8C8D' }].freeze
+
+ attr_reader :project, :client, :errors, :users
def initialize(project)
@project = project
- @client = Client.from_project(@project)
+ @client = Bitbucket::Client.new(project.import_data.credentials)
@formatter = Gitlab::ImportFormatter.new
+ @labels = {}
+ @errors = []
+ @users = {}
end
def execute
- import_issues if has_issues?
+ import_issues
+ import_pull_requests
+ handle_errors
true
- rescue ActiveRecord::RecordInvalid => e
- raise Projects::ImportService::Error.new, e.message
- ensure
- Gitlab::BitbucketImport::KeyDeleter.new(project).execute
end
private
- def gitlab_user_id(project, bitbucket_id)
- if bitbucket_id
- user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s)
- (user && user.id) || project.creator_id
- else
- project.creator_id
- end
+ def handle_errors
+ return unless errors.any?
+
+ project.update_column(:import_error, {
+ message: 'The remote data could not be fully imported.',
+ errors: errors
+ }.to_json)
end
- def identifier
- project.import_source
+ def gitlab_user_id(project, username)
+ find_user_id(username) || project.creator_id
end
- def has_issues?
- client.project(identifier)["has_issues"]
+ def find_user_id(username)
+ return nil unless username
+
+ return users[username] if users.key?(username)
+
+ users[username] = User.select(:id)
+ .joins(:identities)
+ .find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", username)
+ .try(:id)
+ end
+
+ def repo
+ @repo ||= client.repo(project.import_source)
end
def import_issues
- issues = client.issues(identifier)
+ return unless repo.issues_enabled?
+
+ create_labels
+
+ client.issues(repo).each do |issue|
+ begin
+ description = ''
+ description += @formatter.author_line(issue.author) unless find_user_id(issue.author)
+ description += issue.description
+
+ label_name = issue.kind
+ milestone = issue.milestone ? project.milestones.find_or_create_by(title: issue.milestone) : nil
+
+ gitlab_issue = project.issues.create!(
+ iid: issue.iid,
+ title: issue.title,
+ description: description,
+ state: issue.state,
+ author_id: gitlab_user_id(project, issue.author),
+ milestone: milestone,
+ created_at: issue.created_at,
+ updated_at: issue.updated_at
+ )
+
+ gitlab_issue.labels << @labels[label_name]
+
+ import_issue_comments(issue, gitlab_issue) if gitlab_issue.persisted?
+ rescue StandardError => e
+ errors << { type: :issue, iid: issue.iid, errors: e.message }
+ end
+ end
+ end
+
+ def import_issue_comments(issue, gitlab_issue)
+ client.issue_comments(repo, issue.iid).each do |comment|
+ # The note can be blank for issue service messages like "Changed title: ..."
+ # We would like to import those comments as well but there is no any
+ # specific parameter that would allow to process them, it's just an empty comment.
+ # To prevent our importer from just crashing or from creating useless empty comments
+ # we do this check.
+ next unless comment.note.present?
+
+ note = ''
+ note += @formatter.author_line(comment.author) unless find_user_id(comment.author)
+ note += comment.note
+
+ begin
+ gitlab_issue.notes.create!(
+ project: project,
+ note: note,
+ author_id: gitlab_user_id(project, comment.author),
+ created_at: comment.created_at,
+ updated_at: comment.updated_at
+ )
+ rescue StandardError => e
+ errors << { type: :issue_comment, iid: issue.iid, errors: e.message }
+ end
+ end
+ end
- issues.each do |issue|
- body = ''
- reporter = nil
- author = 'Anonymous'
+ def create_labels
+ LABELS.each do |label|
+ @labels[label[:title]] = project.labels.create!(label)
+ end
+ end
- if issue["reported_by"] && issue["reported_by"]["username"]
- reporter = issue["reported_by"]["username"]
- author = reporter
+ def import_pull_requests
+ pull_requests = client.pull_requests(repo)
+
+ pull_requests.each do |pull_request|
+ begin
+ description = ''
+ description += @formatter.author_line(pull_request.author) unless find_user_id(pull_request.author)
+ description += pull_request.description
+
+ merge_request = project.merge_requests.create(
+ iid: pull_request.iid,
+ title: pull_request.title,
+ description: description,
+ source_project: project,
+ source_branch: pull_request.source_branch_name,
+ source_branch_sha: pull_request.source_branch_sha,
+ target_project: project,
+ target_branch: pull_request.target_branch_name,
+ target_branch_sha: pull_request.target_branch_sha,
+ state: pull_request.state,
+ author_id: gitlab_user_id(project, pull_request.author),
+ assignee_id: nil,
+ created_at: pull_request.created_at,
+ updated_at: pull_request.updated_at
+ )
+
+ import_pull_request_comments(pull_request, merge_request) if merge_request.persisted?
+ rescue StandardError => e
+ errors << { type: :pull_request, iid: pull_request.iid, errors: e.message }
end
+ end
+ end
- body = @formatter.author_line(author)
- body += issue["content"]
+ def import_pull_request_comments(pull_request, merge_request)
+ comments = client.pull_request_comments(repo, pull_request.iid)
- comments = client.issue_comments(identifier, issue["local_id"])
+ inline_comments, pr_comments = comments.partition(&:inline?)
- if comments.any?
- body += @formatter.comments_header
- end
+ import_inline_comments(inline_comments, pull_request, merge_request)
+ import_standalone_pr_comments(pr_comments, merge_request)
+ end
- comments.each do |comment|
- author = 'Anonymous'
+ def import_inline_comments(inline_comments, pull_request, merge_request)
+ line_code_map = {}
- if comment["author_info"] && comment["author_info"]["username"]
- author = comment["author_info"]["username"]
- end
+ children, parents = inline_comments.partition(&:has_parent?)
+
+ # The Bitbucket API returns threaded replies as parent-child
+ # relationships. We assume that the child can appear in any order in
+ # the JSON.
+ parents.each do |comment|
+ line_code_map[comment.iid] = generate_line_code(comment)
+ end
- body += @formatter.comment(author, comment["utc_created_on"], comment["content"])
+ children.each do |comment|
+ line_code_map[comment.iid] = line_code_map.fetch(comment.parent_id, nil)
+ end
+
+ inline_comments.each do |comment|
+ begin
+ attributes = pull_request_comment_attributes(comment)
+ attributes.merge!(
+ position: build_position(merge_request, comment),
+ line_code: line_code_map.fetch(comment.iid),
+ type: 'DiffNote')
+
+ merge_request.notes.create!(attributes)
+ rescue StandardError => e
+ errors << { type: :pull_request, iid: comment.iid, errors: e.message }
end
+ end
+ end
- project.issues.create!(
- description: body,
- title: issue["title"],
- state: %w(resolved invalid duplicate wontfix closed).include?(issue["status"]) ? 'closed' : 'opened',
- author_id: gitlab_user_id(project, reporter)
- )
+ def build_position(merge_request, pr_comment)
+ params = {
+ diff_refs: merge_request.diff_refs,
+ old_path: pr_comment.file_path,
+ new_path: pr_comment.file_path,
+ old_line: pr_comment.old_pos,
+ new_line: pr_comment.new_pos
+ }
+
+ Gitlab::Diff::Position.new(params)
+ end
+
+ def import_standalone_pr_comments(pr_comments, merge_request)
+ pr_comments.each do |comment|
+ begin
+ merge_request.notes.create!(pull_request_comment_attributes(comment))
+ rescue StandardError => e
+ errors << { type: :pull_request, iid: comment.iid, errors: e.message }
+ end
end
- rescue ActiveRecord::RecordInvalid => e
- raise Projects::ImportService::Error, e.message
+ end
+
+ def generate_line_code(pr_comment)
+ Gitlab::Diff::LineCode.generate(pr_comment.file_path, pr_comment.new_pos, pr_comment.old_pos)
+ end
+
+ def pull_request_comment_attributes(comment)
+ {
+ project: project,
+ note: comment.note,
+ author_id: gitlab_user_id(project, comment.author),
+ created_at: comment.created_at,
+ updated_at: comment.updated_at
+ }
end
end
end
diff --git a/lib/gitlab/bitbucket_import/key_adder.rb b/lib/gitlab/bitbucket_import/key_adder.rb
deleted file mode 100644
index 0b63f025d0a..00000000000
--- a/lib/gitlab/bitbucket_import/key_adder.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-module Gitlab
- module BitbucketImport
- class KeyAdder
- attr_reader :repo, :current_user, :client
-
- def initialize(repo, current_user, access_params)
- @repo, @current_user = repo, current_user
- @client = Client.new(access_params[:bitbucket_access_token],
- access_params[:bitbucket_access_token_secret])
- end
-
- def execute
- return false unless BitbucketImport.public_key.present?
-
- project_identifier = "#{repo["owner"]}/#{repo["slug"]}"
- client.add_deploy_key(project_identifier, BitbucketImport.public_key)
-
- true
- rescue
- false
- end
- end
- end
-end
diff --git a/lib/gitlab/bitbucket_import/key_deleter.rb b/lib/gitlab/bitbucket_import/key_deleter.rb
deleted file mode 100644
index e03c3155b3e..00000000000
--- a/lib/gitlab/bitbucket_import/key_deleter.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-module Gitlab
- module BitbucketImport
- class KeyDeleter
- attr_reader :project, :current_user, :client
-
- def initialize(project)
- @project = project
- @current_user = project.creator
- @client = Client.from_project(@project)
- end
-
- def execute
- return false unless BitbucketImport.public_key.present?
-
- client.delete_deploy_key(project.import_source, BitbucketImport.public_key)
-
- true
- rescue
- false
- end
- end
- end
-end
diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb
index b90ef0b0fba..eb03882ab26 100644
--- a/lib/gitlab/bitbucket_import/project_creator.rb
+++ b/lib/gitlab/bitbucket_import/project_creator.rb
@@ -1,10 +1,11 @@
module Gitlab
module BitbucketImport
class ProjectCreator
- attr_reader :repo, :namespace, :current_user, :session_data
+ attr_reader :repo, :name, :namespace, :current_user, :session_data
- def initialize(repo, namespace, current_user, session_data)
+ def initialize(repo, name, namespace, current_user, session_data)
@repo = repo
+ @name = name
@namespace = namespace
@current_user = current_user
@session_data = session_data
@@ -13,15 +14,15 @@ module Gitlab
def execute
::Projects::CreateService.new(
current_user,
- name: repo["name"],
- path: repo["slug"],
- description: repo["description"],
+ name: name,
+ path: name,
+ description: repo.description,
namespace_id: namespace.id,
- visibility_level: repo["is_private"] ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC,
- import_type: "bitbucket",
- import_source: "#{repo["owner"]}/#{repo["slug"]}",
- import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git",
- import_data: { credentials: { bb_session: session_data } }
+ visibility_level: repo.visibility_level,
+ import_type: 'bitbucket',
+ import_source: repo.full_name,
+ import_url: repo.clone_url(session_data[:token]),
+ import_data: { credentials: session_data }
).execute
end
end
diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb
new file mode 100644
index 00000000000..25da8474e95
--- /dev/null
+++ b/lib/gitlab/chat_commands/base_command.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ module ChatCommands
+ class BaseCommand
+ QUERY_LIMIT = 5
+
+ def self.match(_text)
+ raise NotImplementedError
+ end
+
+ def self.help_message
+ raise NotImplementedError
+ end
+
+ def self.available?(_project)
+ raise NotImplementedError
+ end
+
+ def self.allowed?(_user, _ability)
+ true
+ end
+
+ def self.can?(object, action, subject)
+ Ability.allowed?(object, action, subject)
+ end
+
+ def execute(_)
+ raise NotImplementedError
+ end
+
+ def collection
+ raise NotImplementedError
+ end
+
+ attr_accessor :project, :current_user, :params
+
+ def initialize(project, user, params = {})
+ @project, @current_user, @params = project, user, params.dup
+ end
+
+ private
+
+ def find_by_iid(iid)
+ collection.find_by(iid: iid)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb
new file mode 100644
index 00000000000..b0d3fdbc48a
--- /dev/null
+++ b/lib/gitlab/chat_commands/command.rb
@@ -0,0 +1,63 @@
+module Gitlab
+ module ChatCommands
+ class Command < BaseCommand
+ COMMANDS = [
+ Gitlab::ChatCommands::IssueShow,
+ Gitlab::ChatCommands::IssueCreate,
+ Gitlab::ChatCommands::IssueSearch,
+ Gitlab::ChatCommands::Deploy,
+ ].freeze
+
+ def execute
+ command, match = match_command
+
+ if command
+ if command.allowed?(project, current_user)
+ present command.new(project, current_user, params).execute(match)
+ else
+ access_denied
+ end
+ else
+ help(help_messages)
+ end
+ end
+
+ private
+
+ def match_command
+ match = nil
+ service = available_commands.find do |klass|
+ match = klass.match(command)
+ end
+
+ [service, match]
+ end
+
+ def help_messages
+ available_commands.map(&:help_message)
+ end
+
+ def available_commands
+ COMMANDS.select do |klass|
+ klass.available?(project)
+ end
+ end
+
+ def command
+ params[:text]
+ end
+
+ def help(messages)
+ Mattermost::Presenter.help(messages, params[:command])
+ end
+
+ def access_denied
+ Mattermost::Presenter.access_denied
+ end
+
+ def present(resource)
+ Mattermost::Presenter.present(resource)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/deploy.rb b/lib/gitlab/chat_commands/deploy.rb
new file mode 100644
index 00000000000..0eed1fce0dc
--- /dev/null
+++ b/lib/gitlab/chat_commands/deploy.rb
@@ -0,0 +1,57 @@
+module Gitlab
+ module ChatCommands
+ class Deploy < BaseCommand
+ include Gitlab::Routing.url_helpers
+
+ def self.match(text)
+ /\Adeploy\s+(?<from>.*)\s+to+\s+(?<to>.*)\z/.match(text)
+ end
+
+ def self.help_message
+ 'deploy <environment> to <target-environment>'
+ end
+
+ def self.available?(project)
+ project.builds_enabled?
+ end
+
+ def self.allowed?(project, user)
+ can?(user, :create_deployment, project)
+ end
+
+ def execute(match)
+ from = match[:from]
+ to = match[:to]
+
+ actions = find_actions(from, to)
+ return unless actions.present?
+
+ if actions.one?
+ play!(from, to, actions.first)
+ else
+ Result.new(:error, 'Too many actions defined')
+ 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)}.")
+ end
+
+ def find_actions(from, to)
+ environment = project.environments.find_by(name: from)
+ 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/issue_command.rb b/lib/gitlab/chat_commands/issue_command.rb
new file mode 100644
index 00000000000..84de3e44c70
--- /dev/null
+++ b/lib/gitlab/chat_commands/issue_command.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module ChatCommands
+ class IssueCommand < BaseCommand
+ def self.available?(project)
+ project.issues_enabled? && project.default_issues_tracker?
+ end
+
+ def collection
+ IssuesFinder.new(current_user, project_id: project.id).execute
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb
new file mode 100644
index 00000000000..cefb6775db8
--- /dev/null
+++ b/lib/gitlab/chat_commands/issue_create.rb
@@ -0,0 +1,26 @@
+module Gitlab
+ module ChatCommands
+ class IssueCreate < IssueCommand
+ def self.match(text)
+ # 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
+
+ def self.help_message
+ 'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>'
+ end
+
+ def self.allowed?(project, user)
+ can?(user, :create_issue, project)
+ end
+
+ def execute(match)
+ title = match[:title]
+ description = match[:description].to_s.rstrip
+
+ Issues::CreateService.new(project, current_user, title: title, description: description).execute
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/chat_commands/issue_search.rb
new file mode 100644
index 00000000000..51bf80c800b
--- /dev/null
+++ b/lib/gitlab/chat_commands/issue_search.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module ChatCommands
+ class IssueSearch < IssueCommand
+ def self.match(text)
+ /\Aissue\s+search\s+(?<query>.*)/.match(text)
+ end
+
+ def self.help_message
+ "issue search <your query>"
+ end
+
+ def execute(match)
+ collection.search(match[:query]).limit(QUERY_LIMIT)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/issue_show.rb b/lib/gitlab/chat_commands/issue_show.rb
new file mode 100644
index 00000000000..2a45d49cf6b
--- /dev/null
+++ b/lib/gitlab/chat_commands/issue_show.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module ChatCommands
+ class IssueShow < IssueCommand
+ def self.match(text)
+ /\Aissue\s+show\s+#{Issue.reference_prefix}?(?<iid>\d+)/.match(text)
+ end
+
+ def self.help_message
+ "issue show <id>"
+ end
+
+ def execute(match)
+ find_by_iid(match[:iid])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/result.rb b/lib/gitlab/chat_commands/result.rb
new file mode 100644
index 00000000000..324d7ef43a3
--- /dev/null
+++ b/lib/gitlab/chat_commands/result.rb
@@ -0,0 +1,5 @@
+module Gitlab
+ module ChatCommands
+ Result = Struct.new(:type, :message)
+ end
+end
diff --git a/lib/gitlab/chat_name_token.rb b/lib/gitlab/chat_name_token.rb
new file mode 100644
index 00000000000..1b081aa9b1d
--- /dev/null
+++ b/lib/gitlab/chat_name_token.rb
@@ -0,0 +1,45 @@
+require 'json'
+
+module Gitlab
+ class ChatNameToken
+ attr_reader :token
+
+ TOKEN_LENGTH = 50
+ EXPIRY_TIME = 10.minutes
+
+ def initialize(token = new_token)
+ @token = token
+ end
+
+ def get
+ Gitlab::Redis.with do |redis|
+ data = redis.get(redis_key)
+ JSON.parse(data, symbolize_names: true) if data
+ end
+ end
+
+ def store!(params)
+ Gitlab::Redis.with do |redis|
+ params = params.to_json
+ redis.set(redis_key, params, ex: EXPIRY_TIME)
+ token
+ end
+ end
+
+ def delete
+ Gitlab::Redis.with do |redis|
+ redis.del(redis_key)
+ end
+ end
+
+ private
+
+ def new_token
+ Devise.friendly_token(TOKEN_LENGTH)
+ end
+
+ def redis_key
+ "gitlab:chat_names:#{token}"
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/credentials/base.rb b/lib/gitlab/ci/build/credentials/base.rb
new file mode 100644
index 00000000000..29a7a27c963
--- /dev/null
+++ b/lib/gitlab/ci/build/credentials/base.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module Ci
+ module Build
+ module Credentials
+ class Base
+ def type
+ self.class.name.demodulize.underscore
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/credentials/factory.rb b/lib/gitlab/ci/build/credentials/factory.rb
new file mode 100644
index 00000000000..2423aa8857d
--- /dev/null
+++ b/lib/gitlab/ci/build/credentials/factory.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Ci
+ module Build
+ module Credentials
+ class Factory
+ def initialize(build)
+ @build = build
+ end
+
+ def create!
+ credentials.select(&:valid?)
+ end
+
+ private
+
+ def credentials
+ providers.map { |provider| provider.new(@build) }
+ end
+
+ def providers
+ [Registry]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/credentials/registry.rb b/lib/gitlab/ci/build/credentials/registry.rb
new file mode 100644
index 00000000000..55eafcaed10
--- /dev/null
+++ b/lib/gitlab/ci/build/credentials/registry.rb
@@ -0,0 +1,24 @@
+module Gitlab
+ module Ci
+ module Build
+ module Credentials
+ class Registry < Base
+ attr_reader :username, :password
+
+ def initialize(build)
+ @username = 'gitlab-ci-token'
+ @password = build.token
+ end
+
+ def url
+ Gitlab.config.registry.host_port
+ end
+
+ def valid?
+ Gitlab.config.registry.enabled
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index bbfa6cf7d05..f7ff7ea212e 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -4,16 +4,10 @@ module Gitlab
# Base GitLab CI Configuration facade
#
class Config
- ##
- # Temporary delegations that should be removed after refactoring
- #
- delegate :before_script, :image, :services, :after_script, :variables,
- :stages, :cache, :jobs, to: :@global
-
def initialize(config)
@config = Loader.new(config).load!
- @global = Node::Global.new(@config)
+ @global = Entry::Global.new(@config)
@global.compose!
end
@@ -28,6 +22,41 @@ module Gitlab
def to_hash
@config
end
+
+ ##
+ # Temporary method that should be removed after refactoring
+ #
+ def before_script
+ @global.before_script_value
+ end
+
+ def image
+ @global.image_value
+ end
+
+ def services
+ @global.services_value
+ end
+
+ def after_script
+ @global.after_script_value
+ end
+
+ def variables
+ @global.variables_value
+ end
+
+ def stages
+ @global.stages_value
+ end
+
+ def cache
+ @global.cache_value
+ end
+
+ def jobs
+ @global.jobs_value
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/artifacts.rb b/lib/gitlab/ci/config/entry/artifacts.rb
new file mode 100644
index 00000000000..b756b0d4555
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/artifacts.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a configuration of job artifacts.
+ #
+ class Artifacts < Node
+ include Validatable
+ include Attributable
+
+ ALLOWED_KEYS = %i[name untracked paths when expire_in]
+
+ attributes ALLOWED_KEYS
+
+ validations do
+ validates :config, type: Hash
+ validates :config, allowed_keys: ALLOWED_KEYS
+
+ with_options allow_nil: true do
+ validates :name, type: String
+ validates :untracked, boolean: true
+ validates :paths, array_of_strings: true
+ validates :when,
+ inclusion: { in: %w[on_success on_failure always],
+ message: 'should be on_success, on_failure ' \
+ 'or always' }
+ validates :expire_in, duration: true
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/attributable.rb b/lib/gitlab/ci/config/entry/attributable.rb
new file mode 100644
index 00000000000..1c8b55ee4c4
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/attributable.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ module Attributable
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def attributes(*attributes)
+ attributes.flatten.each do |attribute|
+ define_method(attribute) do
+ return unless config.is_a?(Hash)
+
+ config[attribute]
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/boolean.rb b/lib/gitlab/ci/config/entry/boolean.rb
new file mode 100644
index 00000000000..f3357f85b99
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/boolean.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a boolean value.
+ #
+ class Boolean < Node
+ include Validatable
+
+ validations do
+ validates :config, boolean: true
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/cache.rb b/lib/gitlab/ci/config/entry/cache.rb
new file mode 100644
index 00000000000..7653cab668b
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/cache.rb
@@ -0,0 +1,29 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a cache configuration
+ #
+ class Cache < Node
+ include Configurable
+
+ ALLOWED_KEYS = %i[key untracked paths]
+
+ validations do
+ validates :config, allowed_keys: ALLOWED_KEYS
+ end
+
+ entry :key, Entry::Key,
+ description: 'Cache key used to define a cache affinity.'
+
+ entry :untracked, Entry::Boolean,
+ description: 'Cache all untracked files.'
+
+ entry :paths, Entry::Paths,
+ description: 'Specify which paths should be cached across builds.'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/commands.rb b/lib/gitlab/ci/config/entry/commands.rb
new file mode 100644
index 00000000000..65d19db249c
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/commands.rb
@@ -0,0 +1,33 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a job script.
+ #
+ class Commands < Node
+ include Validatable
+
+ validations do
+ include LegacyValidationHelpers
+
+ validate do
+ unless string_or_array_of_strings?(config)
+ errors.add(:config,
+ 'should be a string or an array of strings')
+ end
+ end
+
+ def string_or_array_of_strings?(field)
+ validate_string(field) || validate_array_of_strings(field)
+ end
+ end
+
+ def value
+ Array(@config)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/configurable.rb b/lib/gitlab/ci/config/entry/configurable.rb
new file mode 100644
index 00000000000..833ae4a0ff3
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/configurable.rb
@@ -0,0 +1,76 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # This mixin is responsible for adding DSL, which purpose is to
+ # simplifly process of adding child nodes.
+ #
+ # This can be used only if parent node is a configuration entry that
+ # holds a hash as a configuration value, for example:
+ #
+ # job:
+ # script: ...
+ # artifacts: ...
+ #
+ module Configurable
+ extend ActiveSupport::Concern
+ include Validatable
+
+ included do
+ validations do
+ validates :config, type: Hash
+ end
+ end
+
+ def compose!(deps = nil)
+ return unless valid?
+
+ self.class.nodes.each do |key, factory|
+ factory
+ .value(@config[key])
+ .with(key: key, parent: self)
+
+ @entries[key] = factory.create!
+ end
+
+ yield if block_given?
+
+ @entries.each_value do |entry|
+ entry.compose!(deps)
+ end
+ end
+
+ class_methods do
+ def nodes
+ Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }]
+ end
+
+ private # rubocop:disable Lint/UselessAccessModifier
+
+ def entry(key, entry, metadata)
+ factory = Entry::Factory.new(entry)
+ .with(description: metadata[:description])
+
+ (@nodes ||= {}).merge!(key.to_sym => factory)
+ end
+
+ def helpers(*nodes)
+ nodes.each do |symbol|
+ define_method("#{symbol}_defined?") do
+ @entries[symbol].specified? if @entries[symbol]
+ end
+
+ define_method("#{symbol}_value") do
+ return unless @entries[symbol] && @entries[symbol].valid?
+
+ @entries[symbol].value
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb
new file mode 100644
index 00000000000..b7b4b91eb51
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/environment.rb
@@ -0,0 +1,82 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents an environment.
+ #
+ class Environment < Node
+ include Validatable
+
+ ALLOWED_KEYS = %i[name url action on_stop]
+
+ validations do
+ validate do
+ unless hash? || string?
+ errors.add(:config, 'should be a hash or a string')
+ end
+ end
+
+ validates :name, presence: true
+ validates :name,
+ type: {
+ with: String,
+ message: Gitlab::Regex.environment_name_regex_message }
+
+ validates :name,
+ format: {
+ with: Gitlab::Regex.environment_name_regex,
+ message: Gitlab::Regex.environment_name_regex_message }
+
+ with_options if: :hash? do
+ validates :config, allowed_keys: ALLOWED_KEYS
+
+ validates :url,
+ length: { maximum: 255 },
+ addressable_url: true,
+ allow_nil: true
+
+ validates :action,
+ inclusion: { in: %w[start stop], message: 'should be start or stop' },
+ allow_nil: true
+
+ validates :on_stop, type: String, allow_nil: true
+ end
+ end
+
+ def hash?
+ @config.is_a?(Hash)
+ end
+
+ def string?
+ @config.is_a?(String)
+ end
+
+ def name
+ value[:name]
+ end
+
+ def url
+ value[:url]
+ end
+
+ def action
+ value[:action] || 'start'
+ end
+
+ def on_stop
+ value[:on_stop]
+ end
+
+ def value
+ case @config
+ when String then { name: @config, action: 'start' }
+ when Hash then @config
+ else {}
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/factory.rb b/lib/gitlab/ci/config/entry/factory.rb
new file mode 100644
index 00000000000..9f5e393d191
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/factory.rb
@@ -0,0 +1,73 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Factory class responsible for fabricating entry objects.
+ #
+ class Factory
+ class InvalidFactory < StandardError; end
+
+ def initialize(entry)
+ @entry = entry
+ @metadata = {}
+ @attributes = {}
+ end
+
+ def value(value)
+ @value = value
+ self
+ end
+
+ def metadata(metadata)
+ @metadata.merge!(metadata)
+ self
+ end
+
+ def with(attributes)
+ @attributes.merge!(attributes)
+ self
+ end
+
+ def create!
+ raise InvalidFactory unless defined?(@value)
+
+ ##
+ # We assume that unspecified entry is undefined.
+ # See issue #18775.
+ #
+ if @value.nil?
+ Entry::Unspecified.new(
+ fabricate_unspecified
+ )
+ else
+ fabricate(@entry, @value)
+ end
+ end
+
+ private
+
+ def fabricate_unspecified
+ ##
+ # If entry has a default value we fabricate concrete node
+ # with default value.
+ #
+ if @entry.default.nil?
+ fabricate(Entry::Undefined)
+ else
+ fabricate(@entry, @entry.default)
+ end
+ end
+
+ def fabricate(entry, value = nil)
+ entry.new(value, @metadata).tap do |node|
+ node.key = @attributes[:key]
+ node.parent = @attributes[:parent]
+ node.description = @attributes[:description]
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/global.rb b/lib/gitlab/ci/config/entry/global.rb
new file mode 100644
index 00000000000..a4ec8f0ff2f
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/global.rb
@@ -0,0 +1,72 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # This class represents a global entry - root Entry for entire
+ # GitLab CI Configuration file.
+ #
+ class Global < Node
+ include Configurable
+
+ entry :before_script, Entry::Script,
+ description: 'Script that will be executed before each job.'
+
+ entry :image, Entry::Image,
+ description: 'Docker image that will be used to execute jobs.'
+
+ entry :services, Entry::Services,
+ description: 'Docker images that will be linked to the container.'
+
+ entry :after_script, Entry::Script,
+ description: 'Script that will be executed after each job.'
+
+ entry :variables, Entry::Variables,
+ description: 'Environment variables that will be used.'
+
+ entry :stages, Entry::Stages,
+ description: 'Configuration of stages for this pipeline.'
+
+ entry :types, Entry::Stages,
+ description: 'Deprecated: stages for this pipeline.'
+
+ entry :cache, Entry::Cache,
+ description: 'Configure caching between build jobs.'
+
+ helpers :before_script, :image, :services, :after_script,
+ :variables, :stages, :types, :cache, :jobs
+
+ def compose!(_deps = nil)
+ super(self) do
+ compose_jobs!
+ compose_deprecated_entries!
+ end
+ end
+
+ private
+
+ def compose_jobs!
+ factory = Entry::Factory.new(Entry::Jobs)
+ .value(@config.except(*self.class.nodes.keys))
+ .with(key: :jobs, parent: self,
+ description: 'Jobs definition for this pipeline')
+
+ @entries[:jobs] = factory.create!
+ end
+
+ def compose_deprecated_entries!
+ ##
+ # Deprecated `:types` key workaround - if types are defined and
+ # stages are not defined we use types definition as stages.
+ #
+ if types_defined? && !stages_defined?
+ @entries[:stages] = @entries[:types]
+ end
+
+ @entries.delete(:types)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/hidden.rb b/lib/gitlab/ci/config/entry/hidden.rb
new file mode 100644
index 00000000000..6fc3aa385bc
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/hidden.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a hidden CI/CD key.
+ #
+ class Hidden < Node
+ include Validatable
+
+ validations do
+ validates :config, presence: true
+ end
+
+ def relevant?
+ false
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb
new file mode 100644
index 00000000000..b5050257688
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/image.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a Docker image.
+ #
+ class Image < Node
+ include Validatable
+
+ validations do
+ validates :config, type: String
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
new file mode 100644
index 00000000000..a55362f0b6b
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -0,0 +1,140 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a concrete CI/CD job.
+ #
+ class Job < Node
+ include Configurable
+ include Attributable
+
+ ALLOWED_KEYS = %i[tags script only except type image services allow_failure
+ type stage when artifacts cache dependencies before_script
+ after_script variables environment]
+
+ validations do
+ validates :config, allowed_keys: ALLOWED_KEYS
+ validates :config, presence: true
+ validates :script, presence: true
+ validates :name, presence: true
+ validates :name, type: Symbol
+
+ with_options allow_nil: true do
+ validates :tags, array_of_strings: true
+ validates :allow_failure, boolean: true
+ validates :when,
+ inclusion: { in: %w[on_success on_failure always manual],
+ message: 'should be on_success, on_failure, ' \
+ 'always or manual' }
+
+ validates :dependencies, array_of_strings: true
+ end
+ end
+
+ entry :before_script, Entry::Script,
+ description: 'Global before script overridden in this job.'
+
+ entry :script, Entry::Commands,
+ description: 'Commands that will be executed in this job.'
+
+ entry :stage, Entry::Stage,
+ description: 'Pipeline stage this job will be executed into.'
+
+ entry :type, Entry::Stage,
+ description: 'Deprecated: stage this job will be executed into.'
+
+ entry :after_script, Entry::Script,
+ description: 'Commands that will be executed when finishing job.'
+
+ entry :cache, Entry::Cache,
+ description: 'Cache definition for this job.'
+
+ entry :image, Entry::Image,
+ description: 'Image that will be used to execute this job.'
+
+ entry :services, Entry::Services,
+ description: 'Services that will be used to execute this job.'
+
+ entry :only, Entry::Trigger,
+ description: 'Refs policy this job will be executed for.'
+
+ entry :except, Entry::Trigger,
+ description: 'Refs policy this job will be executed for.'
+
+ entry :variables, Entry::Variables,
+ description: 'Environment variables available for this job.'
+
+ entry :artifacts, Entry::Artifacts,
+ description: 'Artifacts configuration for this job.'
+
+ entry :environment, Entry::Environment,
+ description: 'Environment configuration for this job.'
+
+ helpers :before_script, :script, :stage, :type, :after_script,
+ :cache, :image, :services, :only, :except, :variables,
+ :artifacts, :commands, :environment
+
+ attributes :script, :tags, :allow_failure, :when, :dependencies
+
+ def compose!(deps = nil)
+ super do
+ if type_defined? && !stage_defined?
+ @entries[:stage] = @entries[:type]
+ end
+
+ @entries.delete(:type)
+ end
+
+ inherit!(deps)
+ end
+
+ def name
+ @metadata[:name]
+ end
+
+ def value
+ @config.merge(to_hash.compact)
+ end
+
+ def commands
+ (before_script_value.to_a + script_value.to_a).join("\n")
+ end
+
+ private
+
+ def inherit!(deps)
+ return unless deps
+
+ self.class.nodes.each_key do |key|
+ global_entry = deps[key]
+ job_entry = self[key]
+
+ if global_entry.specified? && !job_entry.specified?
+ @entries[key] = global_entry
+ end
+ end
+ end
+
+ def to_hash
+ { name: name,
+ before_script: before_script_value,
+ script: script_value,
+ commands: commands,
+ image: image_value,
+ services: services_value,
+ stage: stage_value,
+ cache: cache_value,
+ only: only_value,
+ except: except_value,
+ variables: variables_defined? ? variables_value : nil,
+ environment: environment_defined? ? environment_value : nil,
+ environment_name: environment_defined? ? environment_value[:name] : nil,
+ artifacts: artifacts_value,
+ after_script: after_script_value }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/jobs.rb b/lib/gitlab/ci/config/entry/jobs.rb
new file mode 100644
index 00000000000..5671a09480b
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/jobs.rb
@@ -0,0 +1,52 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a set of jobs.
+ #
+ class Jobs < Node
+ include Validatable
+
+ validations do
+ validates :config, type: Hash
+
+ validate do
+ unless has_visible_job?
+ errors.add(:config, 'should contain at least one visible job')
+ end
+ end
+
+ def has_visible_job?
+ config.any? { |name, _| !hidden?(name) }
+ end
+ end
+
+ def hidden?(name)
+ name.to_s.start_with?('.')
+ end
+
+ def compose!(deps = nil)
+ super do
+ @config.each do |name, config|
+ node = hidden?(name) ? Entry::Hidden : Entry::Job
+
+ factory = Entry::Factory.new(node)
+ .value(config || {})
+ .metadata(name: name)
+ .with(key: name, parent: self,
+ description: "#{name} job definition.")
+
+ @entries[name] = factory.create!
+ end
+
+ @entries.each_value do |entry|
+ entry.compose!(deps)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/key.rb b/lib/gitlab/ci/config/entry/key.rb
new file mode 100644
index 00000000000..0e4c9fe6edc
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/key.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a key.
+ #
+ class Key < Node
+ include Validatable
+
+ validations do
+ validates :config, key: true
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
new file mode 100644
index 00000000000..f01975aab5c
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
@@ -0,0 +1,51 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ module LegacyValidationHelpers
+ private
+
+ def validate_duration(value)
+ value.is_a?(String) && ChronicDuration.parse(value)
+ rescue ChronicDuration::DurationParseError
+ false
+ end
+
+ def validate_array_of_strings(values)
+ values.is_a?(Array) && values.all? { |value| validate_string(value) }
+ end
+
+ def validate_array_of_strings_or_regexps(values)
+ values.is_a?(Array) && values.all? { |value| validate_string_or_regexp(value) }
+ end
+
+ def validate_variables(variables)
+ variables.is_a?(Hash) &&
+ variables.all? { |key, value| validate_string(key) && validate_string(value) }
+ end
+
+ def validate_string(value)
+ value.is_a?(String) || value.is_a?(Symbol)
+ 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])
+ else
+ true
+ end
+ rescue RegexpError
+ false
+ end
+
+ def validate_boolean(value)
+ value.in?([true, false])
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/node.rb b/lib/gitlab/ci/config/entry/node.rb
new file mode 100644
index 00000000000..5eef2868cd6
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/node.rb
@@ -0,0 +1,83 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Base abstract class for each configuration entry node.
+ #
+ class Node
+ class InvalidError < StandardError; end
+
+ attr_reader :config, :metadata
+ attr_accessor :key, :parent, :description
+
+ def initialize(config, **metadata)
+ @config = config
+ @metadata = metadata
+ @entries = {}
+
+ @validator = self.class.validator.new(self)
+ @validator.validate(:new)
+ end
+
+ def [](key)
+ @entries[key] || Entry::Undefined.new
+ end
+
+ def compose!(deps = nil)
+ return unless valid?
+
+ yield if block_given?
+ end
+
+ def leaf?
+ @entries.none?
+ end
+
+ def descendants
+ @entries.values
+ end
+
+ def ancestors
+ @parent ? @parent.ancestors + [@parent] : []
+ end
+
+ def valid?
+ errors.none?
+ end
+
+ def errors
+ @validator.messages + descendants.flat_map(&:errors)
+ end
+
+ def value
+ if leaf?
+ @config
+ else
+ meaningful = @entries.select do |_key, value|
+ value.specified? && value.relevant?
+ end
+
+ Hash[meaningful.map { |key, entry| [key, entry.value] }]
+ end
+ end
+
+ def specified?
+ true
+ end
+
+ def relevant?
+ true
+ end
+
+ def self.default
+ end
+
+ def self.validator
+ Validator
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/paths.rb b/lib/gitlab/ci/config/entry/paths.rb
new file mode 100644
index 00000000000..68dad161149
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/paths.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents an array of paths.
+ #
+ class Paths < Node
+ include Validatable
+
+ validations do
+ validates :config, array_of_strings: true
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/script.rb b/lib/gitlab/ci/config/entry/script.rb
new file mode 100644
index 00000000000..29ecd9995ca
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/script.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a script.
+ #
+ class Script < Node
+ include Validatable
+
+ validations do
+ validates :config, array_of_strings: true
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/services.rb b/lib/gitlab/ci/config/entry/services.rb
new file mode 100644
index 00000000000..84f8ab780f5
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/services.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a configuration of Docker services.
+ #
+ class Services < Node
+ include Validatable
+
+ validations do
+ validates :config, array_of_strings: true
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/stage.rb b/lib/gitlab/ci/config/entry/stage.rb
new file mode 100644
index 00000000000..b7afaba1de8
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/stage.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a stage for a job.
+ #
+ class Stage < Node
+ include Validatable
+
+ validations do
+ validates :config, type: String
+ end
+
+ def self.default
+ 'test'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/stages.rb b/lib/gitlab/ci/config/entry/stages.rb
new file mode 100644
index 00000000000..ec187bd3732
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/stages.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a configuration for pipeline stages.
+ #
+ class Stages < Node
+ include Validatable
+
+ validations do
+ validates :config, array_of_strings: true
+ end
+
+ def self.default
+ %w[build test deploy]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/trigger.rb b/lib/gitlab/ci/config/entry/trigger.rb
new file mode 100644
index 00000000000..28b0a9ffe01
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/trigger.rb
@@ -0,0 +1,26 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a trigger policy for the job.
+ #
+ class Trigger < Node
+ 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
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/undefined.rb b/lib/gitlab/ci/config/entry/undefined.rb
new file mode 100644
index 00000000000..b33b8238230
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/undefined.rb
@@ -0,0 +1,36 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # This class represents an undefined entry.
+ #
+ class Undefined < Node
+ def initialize(*)
+ super(nil)
+ end
+
+ def value
+ nil
+ end
+
+ def valid?
+ true
+ end
+
+ def errors
+ []
+ end
+
+ def specified?
+ false
+ end
+
+ def relevant?
+ false
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/unspecified.rb b/lib/gitlab/ci/config/entry/unspecified.rb
new file mode 100644
index 00000000000..fbb2551e870
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/unspecified.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # This class represents an unspecified entry.
+ #
+ # It decorates original entry adding method that indicates it is
+ # unspecified.
+ #
+ class Unspecified < SimpleDelegator
+ def specified?
+ false
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/validatable.rb b/lib/gitlab/ci/config/entry/validatable.rb
new file mode 100644
index 00000000000..f7f1b111571
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/validatable.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ module Validatable
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def validator
+ @validator ||= Class.new(Entry::Validator).tap do |validator|
+ if defined?(@validations)
+ @validations.each { |rules| validator.class_eval(&rules) }
+ end
+ end
+ end
+
+ private
+
+ def validations(&block)
+ (@validations ||= []).append(block)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/validator.rb b/lib/gitlab/ci/config/entry/validator.rb
new file mode 100644
index 00000000000..55343005fe3
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/validator.rb
@@ -0,0 +1,42 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ class Validator < SimpleDelegator
+ include ActiveModel::Validations
+ include Entry::Validators
+
+ def initialize(entry)
+ super(entry)
+ @entry = entry
+ end
+
+ def messages
+ errors.full_messages.map do |error|
+ "#{location} #{error}".downcase
+ end
+ end
+
+ def self.name
+ 'Validator'
+ end
+
+ private
+
+ def location
+ predecessors = ancestors.map(&:key).compact
+ predecessors.append(key_name).join(':')
+ end
+
+ def key_name
+ if key.blank?
+ @entry.class.name.demodulize.underscore.humanize
+ else
+ key
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb
new file mode 100644
index 00000000000..8632dd0e233
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/validators.rb
@@ -0,0 +1,82 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ module Validators
+ class AllowedKeysValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unknown_keys = record.config.try(:keys).to_a - options[:in]
+
+ if unknown_keys.any?
+ record.errors.add(:config, 'contains unknown keys: ' +
+ unknown_keys.join(', '))
+ end
+ end
+ end
+
+ class ArrayOfStringsValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_array_of_strings(value)
+ record.errors.add(attribute, 'should be an array of strings')
+ end
+ end
+ end
+
+ class BooleanValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_boolean(value)
+ record.errors.add(attribute, 'should be a boolean value')
+ end
+ end
+ end
+
+ class DurationValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_duration(value)
+ record.errors.add(attribute, 'should be a duration')
+ end
+ end
+ end
+
+ class KeyValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_string(value)
+ record.errors.add(attribute, 'should be a string or symbol')
+ end
+ end
+ end
+
+ class TypeValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ type = options[:with]
+ raise unless type.is_a?(Class)
+
+ unless value.is_a?(type)
+ message = options[:message] || "should be a #{type.name}"
+ record.errors.add(attribute, message)
+ end
+ end
+ end
+
+ class VariablesValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_variables(value)
+ record.errors.add(attribute, 'should be a hash of key value pairs')
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/variables.rb b/lib/gitlab/ci/config/entry/variables.rb
new file mode 100644
index 00000000000..c3b0e651c3a
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/variables.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents environment variables.
+ #
+ class Variables < Node
+ include Validatable
+
+ validations do
+ validates :config, variables: true
+ end
+
+ def self.default
+ {}
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/artifacts.rb b/lib/gitlab/ci/config/node/artifacts.rb
deleted file mode 100644
index 844bd2fe998..00000000000
--- a/lib/gitlab/ci/config/node/artifacts.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # Entry that represents a configuration of job artifacts.
- #
- class Artifacts < Entry
- include Validatable
- include Attributable
-
- ALLOWED_KEYS = %i[name untracked paths when expire_in]
-
- attributes ALLOWED_KEYS
-
- validations do
- validates :config, type: Hash
- validates :config, allowed_keys: ALLOWED_KEYS
-
- with_options allow_nil: true do
- validates :name, type: String
- validates :untracked, boolean: true
- validates :paths, array_of_strings: true
- validates :when,
- inclusion: { in: %w[on_success on_failure always],
- message: 'should be on_success, on_failure ' \
- 'or always' }
- validates :expire_in, duration: true
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/attributable.rb b/lib/gitlab/ci/config/node/attributable.rb
deleted file mode 100644
index 221b666f9f6..00000000000
--- a/lib/gitlab/ci/config/node/attributable.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- module Attributable
- extend ActiveSupport::Concern
-
- class_methods do
- def attributes(*attributes)
- attributes.flatten.each do |attribute|
- define_method(attribute) do
- return unless config.is_a?(Hash)
-
- config[attribute]
- end
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/boolean.rb b/lib/gitlab/ci/config/node/boolean.rb
deleted file mode 100644
index 84b03ee7832..00000000000
--- a/lib/gitlab/ci/config/node/boolean.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # Entry that represents a boolean value.
- #
- class Boolean < Entry
- include Validatable
-
- validations do
- validates :config, boolean: true
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/cache.rb b/lib/gitlab/ci/config/node/cache.rb
deleted file mode 100644
index b4bda2841ac..00000000000
--- a/lib/gitlab/ci/config/node/cache.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # Entry that represents a cache configuration
- #
- class Cache < Entry
- include Configurable
-
- ALLOWED_KEYS = %i[key untracked paths]
-
- validations do
- validates :config, allowed_keys: ALLOWED_KEYS
- end
-
- node :key, Node::Key,
- description: 'Cache key used to define a cache affinity.'
-
- node :untracked, Node::Boolean,
- description: 'Cache all untracked files.'
-
- node :paths, Node::Paths,
- description: 'Specify which paths should be cached across builds.'
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/commands.rb b/lib/gitlab/ci/config/node/commands.rb
deleted file mode 100644
index d7657ae314b..00000000000
--- a/lib/gitlab/ci/config/node/commands.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # Entry that represents a job script.
- #
- class Commands < Entry
- include Validatable
-
- validations do
- include LegacyValidationHelpers
-
- validate do
- unless string_or_array_of_strings?(config)
- errors.add(:config,
- 'should be a string or an array of strings')
- end
- end
-
- def string_or_array_of_strings?(field)
- validate_string(field) || validate_array_of_strings(field)
- end
- end
-
- def value
- Array(@config)
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb
deleted file mode 100644
index 6b7ab2fdaf2..00000000000
--- a/lib/gitlab/ci/config/node/configurable.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # This mixin is responsible for adding DSL, which purpose is to
- # simplifly process of adding child nodes.
- #
- # This can be used only if parent node is a configuration entry that
- # holds a hash as a configuration value, for example:
- #
- # job:
- # script: ...
- # artifacts: ...
- #
- module Configurable
- extend ActiveSupport::Concern
- include Validatable
-
- included do
- validations do
- validates :config, type: Hash
- end
- end
-
- def compose!(deps = nil)
- return unless valid?
-
- self.class.nodes.each do |key, factory|
- factory
- .value(@config[key])
- .with(key: key, parent: self)
-
- @entries[key] = factory.create!
- end
-
- yield if block_given?
-
- @entries.each_value do |entry|
- entry.compose!(deps)
- end
- end
-
- class_methods do
- def nodes
- Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }]
- end
-
- private # rubocop:disable Lint/UselessAccessModifier
-
- def node(key, node, metadata)
- factory = Node::Factory.new(node)
- .with(description: metadata[:description])
-
- (@nodes ||= {}).merge!(key.to_sym => factory)
- end
-
- def helpers(*nodes)
- nodes.each do |symbol|
- define_method("#{symbol}_defined?") do
- @entries[symbol].specified? if @entries[symbol]
- end
-
- define_method("#{symbol}_value") do
- return unless @entries[symbol] && @entries[symbol].valid?
-
- @entries[symbol].value
- end
-
- alias_method symbol.to_sym, "#{symbol}_value".to_sym
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb
deleted file mode 100644
index 8717eabf81e..00000000000
--- a/lib/gitlab/ci/config/node/entry.rb
+++ /dev/null
@@ -1,83 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # Base abstract class for each configuration entry node.
- #
- class Entry
- class InvalidError < StandardError; end
-
- attr_reader :config, :metadata
- attr_accessor :key, :parent, :description
-
- def initialize(config, **metadata)
- @config = config
- @metadata = metadata
- @entries = {}
-
- @validator = self.class.validator.new(self)
- @validator.validate(:new)
- end
-
- def [](key)
- @entries[key] || Node::Undefined.new
- end
-
- def compose!(deps = nil)
- return unless valid?
-
- yield if block_given?
- end
-
- def leaf?
- @entries.none?
- end
-
- def descendants
- @entries.values
- end
-
- def ancestors
- @parent ? @parent.ancestors + [@parent] : []
- end
-
- def valid?
- errors.none?
- end
-
- def errors
- @validator.messages + descendants.flat_map(&:errors)
- end
-
- def value
- if leaf?
- @config
- else
- meaningful = @entries.select do |_key, value|
- value.specified? && value.relevant?
- end
-
- Hash[meaningful.map { |key, entry| [key, entry.value] }]
- end
- end
-
- def specified?
- true
- end
-
- def relevant?
- true
- end
-
- def self.default
- end
-
- def self.validator
- Validator
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/environment.rb b/lib/gitlab/ci/config/node/environment.rb
deleted file mode 100644
index 9a95ef43628..00000000000
--- a/lib/gitlab/ci/config/node/environment.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # Entry that represents an environment.
- #
- class Environment < Entry
- include Validatable
-
- ALLOWED_KEYS = %i[name url action on_stop]
-
- validations do
- validate do
- unless hash? || string?
- errors.add(:config, 'should be a hash or a string')
- end
- end
-
- validates :name, presence: true
- validates :name,
- type: {
- with: String,
- message: Gitlab::Regex.environment_name_regex_message }
-
- validates :name,
- format: {
- with: Gitlab::Regex.environment_name_regex,
- message: Gitlab::Regex.environment_name_regex_message }
-
- with_options if: :hash? do
- validates :config, allowed_keys: ALLOWED_KEYS
-
- validates :url,
- length: { maximum: 255 },
- addressable_url: true,
- allow_nil: true
-
- validates :action,
- inclusion: { in: %w[start stop], message: 'should be start or stop' },
- allow_nil: true
-
- validates :on_stop, type: String, allow_nil: true
- end
- end
-
- def hash?
- @config.is_a?(Hash)
- end
-
- def string?
- @config.is_a?(String)
- end
-
- def name
- value[:name]
- end
-
- def url
- value[:url]
- end
-
- def action
- value[:action] || 'start'
- end
-
- def on_stop
- value[:on_stop]
- end
-
- def value
- case @config
- when String then { name: @config, action: 'start' }
- when Hash then @config
- else {}
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/factory.rb b/lib/gitlab/ci/config/node/factory.rb
deleted file mode 100644
index 5387f29ad59..00000000000
--- a/lib/gitlab/ci/config/node/factory.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # Factory class responsible for fabricating node entry objects.
- #
- class Factory
- class InvalidFactory < StandardError; end
-
- def initialize(node)
- @node = node
- @metadata = {}
- @attributes = {}
- end
-
- def value(value)
- @value = value
- self
- end
-
- def metadata(metadata)
- @metadata.merge!(metadata)
- self
- end
-
- def with(attributes)
- @attributes.merge!(attributes)
- self
- end
-
- def create!
- raise InvalidFactory unless defined?(@value)
-
- ##
- # We assume that unspecified entry is undefined.
- # See issue #18775.
- #
- if @value.nil?
- Node::Unspecified.new(
- fabricate_unspecified
- )
- else
- fabricate(@node, @value)
- end
- end
-
- private
-
- def fabricate_unspecified
- ##
- # If node has a default value we fabricate concrete node
- # with default value.
- #
- if @node.default.nil?
- fabricate(Node::Undefined)
- else
- fabricate(@node, @node.default)
- end
- end
-
- def fabricate(node, value = nil)
- node.new(value, @metadata).tap do |entry|
- entry.key = @attributes[:key]
- entry.parent = @attributes[:parent]
- entry.description = @attributes[:description]
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb
deleted file mode 100644
index 2a2943c9288..00000000000
--- a/lib/gitlab/ci/config/node/global.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # This class represents a global entry - root node for entire
- # GitLab CI Configuration file.
- #
- class Global < Entry
- include Configurable
-
- node :before_script, Node::Script,
- description: 'Script that will be executed before each job.'
-
- node :image, Node::Image,
- description: 'Docker image that will be used to execute jobs.'
-
- node :services, Node::Services,
- description: 'Docker images that will be linked to the container.'
-
- node :after_script, Node::Script,
- description: 'Script that will be executed after each job.'
-
- node :variables, Node::Variables,
- description: 'Environment variables that will be used.'
-
- node :stages, Node::Stages,
- description: 'Configuration of stages for this pipeline.'
-
- node :types, Node::Stages,
- description: 'Deprecated: stages for this pipeline.'
-
- node :cache, Node::Cache,
- description: 'Configure caching between build jobs.'
-
- helpers :before_script, :image, :services, :after_script,
- :variables, :stages, :types, :cache, :jobs
-
- def compose!(_deps = nil)
- super(self) do
- compose_jobs!
- compose_deprecated_entries!
- end
- end
-
- private
-
- def compose_jobs!
- factory = Node::Factory.new(Node::Jobs)
- .value(@config.except(*self.class.nodes.keys))
- .with(key: :jobs, parent: self,
- description: 'Jobs definition for this pipeline')
-
- @entries[:jobs] = factory.create!
- end
-
- def compose_deprecated_entries!
- ##
- # Deprecated `:types` key workaround - if types are defined and
- # stages are not defined we use types definition as stages.
- #
- if types_defined? && !stages_defined?
- @entries[:stages] = @entries[:types]
- end
-
- @entries.delete(:types)
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/hidden.rb b/lib/gitlab/ci/config/node/hidden.rb
deleted file mode 100644
index fe4ee8a7fc6..00000000000
--- a/lib/gitlab/ci/config/node/hidden.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # Entry that represents a hidden CI/CD job.
- #
- class Hidden < Entry
- include Validatable
-
- validations do
- validates :config, presence: true
- end
-
- def relevant?
- false
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/image.rb b/lib/gitlab/ci/config/node/image.rb
deleted file mode 100644
index 5d3c7c5eab0..00000000000
--- a/lib/gitlab/ci/config/node/image.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # Entry that represents a Docker image.
- #
- class Image < Entry
- include Validatable
-
- validations do
- validates :config, type: String
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb
deleted file mode 100644
index 603334d6793..00000000000
--- a/lib/gitlab/ci/config/node/job.rb
+++ /dev/null
@@ -1,140 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # Entry that represents a concrete CI/CD job.
- #
- class Job < Entry
- include Configurable
- include Attributable
-
- ALLOWED_KEYS = %i[tags script only except type image services allow_failure
- type stage when artifacts cache dependencies before_script
- after_script variables environment]
-
- attributes :tags, :allow_failure, :when, :dependencies
-
- validations do
- validates :config, allowed_keys: ALLOWED_KEYS
-
- validates :config, presence: true
- validates :name, presence: true
- validates :name, type: Symbol
-
- with_options allow_nil: true do
- validates :tags, array_of_strings: true
- validates :allow_failure, boolean: true
- validates :when,
- inclusion: { in: %w[on_success on_failure always manual],
- message: 'should be on_success, on_failure, ' \
- 'always or manual' }
-
- validates :dependencies, array_of_strings: true
- end
- end
-
- node :before_script, Node::Script,
- description: 'Global before script overridden in this job.'
-
- node :script, Node::Commands,
- description: 'Commands that will be executed in this job.'
-
- node :stage, Node::Stage,
- description: 'Pipeline stage this job will be executed into.'
-
- node :type, Node::Stage,
- description: 'Deprecated: stage this job will be executed into.'
-
- node :after_script, Node::Script,
- description: 'Commands that will be executed when finishing job.'
-
- node :cache, Node::Cache,
- description: 'Cache definition for this job.'
-
- node :image, Node::Image,
- description: 'Image that will be used to execute this job.'
-
- node :services, Node::Services,
- description: 'Services that will be used to execute this job.'
-
- node :only, Node::Trigger,
- description: 'Refs policy this job will be executed for.'
-
- node :except, Node::Trigger,
- description: 'Refs policy this job will be executed for.'
-
- node :variables, Node::Variables,
- description: 'Environment variables available for this job.'
-
- node :artifacts, Node::Artifacts,
- description: 'Artifacts configuration for this job.'
-
- node :environment, Node::Environment,
- description: 'Environment configuration for this job.'
-
- helpers :before_script, :script, :stage, :type, :after_script,
- :cache, :image, :services, :only, :except, :variables,
- :artifacts, :commands, :environment
-
- def compose!(deps = nil)
- super do
- if type_defined? && !stage_defined?
- @entries[:stage] = @entries[:type]
- end
-
- @entries.delete(:type)
- end
-
- inherit!(deps)
- end
-
- def name
- @metadata[:name]
- end
-
- def value
- @config.merge(to_hash.compact)
- end
-
- def commands
- (before_script_value.to_a + script_value.to_a).join("\n")
- end
-
- private
-
- def inherit!(deps)
- return unless deps
-
- self.class.nodes.each_key do |key|
- global_entry = deps[key]
- job_entry = @entries[key]
-
- if global_entry.specified? && !job_entry.specified?
- @entries[key] = global_entry
- end
- end
- end
-
- def to_hash
- { name: name,
- before_script: before_script,
- script: script,
- commands: commands,
- image: image,
- services: services,
- stage: stage,
- cache: cache,
- only: only,
- except: except,
- variables: variables_defined? ? variables : nil,
- environment: environment_defined? ? environment : nil,
- environment_name: environment_defined? ? environment[:name] : nil,
- artifacts: artifacts,
- after_script: after_script }
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb
deleted file mode 100644
index d10e80d1a7d..00000000000
--- a/lib/gitlab/ci/config/node/jobs.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # Entry that represents a set of jobs.
- #
- class Jobs < Entry
- include Validatable
-
- validations do
- validates :config, type: Hash
-
- validate do
- unless has_visible_job?
- errors.add(:config, 'should contain at least one visible job')
- end
- end
-
- def has_visible_job?
- config.any? { |name, _| !hidden?(name) }
- end
- end
-
- def hidden?(name)
- name.to_s.start_with?('.')
- end
-
- def compose!(deps = nil)
- super do
- @config.each do |name, config|
- node = hidden?(name) ? Node::Hidden : Node::Job
-
- factory = Node::Factory.new(node)
- .value(config || {})
- .metadata(name: name)
- .with(key: name, parent: self,
- description: "#{name} job definition.")
-
- @entries[name] = factory.create!
- end
-
- @entries.each_value do |entry|
- entry.compose!(deps)
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/key.rb b/lib/gitlab/ci/config/node/key.rb
deleted file mode 100644
index f8b461ca098..00000000000
--- a/lib/gitlab/ci/config/node/key.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # Entry that represents a key.
- #
- class Key < Entry
- include Validatable
-
- validations do
- validates :config, key: true
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/legacy_validation_helpers.rb b/lib/gitlab/ci/config/node/legacy_validation_helpers.rb
deleted file mode 100644
index 0c291efe6a5..00000000000
--- a/lib/gitlab/ci/config/node/legacy_validation_helpers.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- module LegacyValidationHelpers
- private
-
- def validate_duration(value)
- value.is_a?(String) && ChronicDuration.parse(value)
- rescue ChronicDuration::DurationParseError
- false
- end
-
- def validate_array_of_strings(values)
- values.is_a?(Array) && values.all? { |value| validate_string(value) }
- end
-
- def validate_array_of_strings_or_regexps(values)
- values.is_a?(Array) && values.all? { |value| validate_string_or_regexp(value) }
- end
-
- def validate_variables(variables)
- variables.is_a?(Hash) &&
- variables.all? { |key, value| validate_string(key) && validate_string(value) }
- end
-
- def validate_string(value)
- value.is_a?(String) || value.is_a?(Symbol)
- 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])
- else
- true
- end
- rescue RegexpError
- false
- end
-
- def validate_boolean(value)
- value.in?([true, false])
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/paths.rb b/lib/gitlab/ci/config/node/paths.rb
deleted file mode 100644
index 3c6d3a52966..00000000000
--- a/lib/gitlab/ci/config/node/paths.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # Entry that represents an array of paths.
- #
- class Paths < Entry
- include Validatable
-
- validations do
- validates :config, array_of_strings: true
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/script.rb b/lib/gitlab/ci/config/node/script.rb
deleted file mode 100644
index 39328f0fade..00000000000
--- a/lib/gitlab/ci/config/node/script.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # Entry that represents a script.
- #
- class Script < Entry
- include Validatable
-
- validations do
- validates :config, array_of_strings: true
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/services.rb b/lib/gitlab/ci/config/node/services.rb
deleted file mode 100644
index 481e2b66adc..00000000000
--- a/lib/gitlab/ci/config/node/services.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # Entry that represents a configuration of Docker services.
- #
- class Services < Entry
- include Validatable
-
- validations do
- validates :config, array_of_strings: true
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/stage.rb b/lib/gitlab/ci/config/node/stage.rb
deleted file mode 100644
index cbc97641f5a..00000000000
--- a/lib/gitlab/ci/config/node/stage.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # Entry that represents a stage for a job.
- #
- class Stage < Entry
- include Validatable
-
- validations do
- validates :config, type: String
- end
-
- def self.default
- 'test'
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/stages.rb b/lib/gitlab/ci/config/node/stages.rb
deleted file mode 100644
index b1fe45357ff..00000000000
--- a/lib/gitlab/ci/config/node/stages.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # Entry that represents a configuration for pipeline stages.
- #
- class Stages < Entry
- include Validatable
-
- validations do
- validates :config, array_of_strings: true
- end
-
- def self.default
- %w[build test deploy]
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/trigger.rb b/lib/gitlab/ci/config/node/trigger.rb
deleted file mode 100644
index d8b31975088..00000000000
--- a/lib/gitlab/ci/config/node/trigger.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # Entry that represents a trigger policy for the job.
- #
- class Trigger < Entry
- 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
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/undefined.rb b/lib/gitlab/ci/config/node/undefined.rb
deleted file mode 100644
index 33e78023539..00000000000
--- a/lib/gitlab/ci/config/node/undefined.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # This class represents an undefined node.
- #
- # Implements the Null Object pattern.
- #
- class Undefined < Entry
- def initialize(*)
- super(nil)
- end
-
- def value
- nil
- end
-
- def valid?
- true
- end
-
- def errors
- []
- end
-
- def specified?
- false
- end
-
- def relevant?
- false
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/unspecified.rb b/lib/gitlab/ci/config/node/unspecified.rb
deleted file mode 100644
index a7d1f6131b8..00000000000
--- a/lib/gitlab/ci/config/node/unspecified.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # This class represents an unspecified entry node.
- #
- # It decorates original entry adding method that indicates it is
- # unspecified.
- #
- class Unspecified < SimpleDelegator
- def specified?
- false
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/validatable.rb b/lib/gitlab/ci/config/node/validatable.rb
deleted file mode 100644
index 085e6e988d1..00000000000
--- a/lib/gitlab/ci/config/node/validatable.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- module Validatable
- extend ActiveSupport::Concern
-
- class_methods do
- def validator
- @validator ||= Class.new(Node::Validator).tap do |validator|
- if defined?(@validations)
- @validations.each { |rules| validator.class_eval(&rules) }
- end
- end
- end
-
- private
-
- def validations(&block)
- (@validations ||= []).append(block)
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/validator.rb b/lib/gitlab/ci/config/node/validator.rb
deleted file mode 100644
index 43c7e102b50..00000000000
--- a/lib/gitlab/ci/config/node/validator.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- class Validator < SimpleDelegator
- include ActiveModel::Validations
- include Node::Validators
-
- def initialize(node)
- super(node)
- @node = node
- end
-
- def messages
- errors.full_messages.map do |error|
- "#{location} #{error}".downcase
- end
- end
-
- def self.name
- 'Validator'
- end
-
- private
-
- def location
- predecessors = ancestors.map(&:key).compact
- predecessors.append(key_name).join(':')
- end
-
- def key_name
- if key.blank?
- @node.class.name.demodulize.underscore.humanize
- else
- key
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/validators.rb b/lib/gitlab/ci/config/node/validators.rb
deleted file mode 100644
index e20908ad3cb..00000000000
--- a/lib/gitlab/ci/config/node/validators.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- module Validators
- class AllowedKeysValidator < ActiveModel::EachValidator
- def validate_each(record, attribute, value)
- unknown_keys = record.config.try(:keys).to_a - options[:in]
-
- if unknown_keys.any?
- record.errors.add(:config, 'contains unknown keys: ' +
- unknown_keys.join(', '))
- end
- end
- end
-
- class ArrayOfStringsValidator < ActiveModel::EachValidator
- include LegacyValidationHelpers
-
- def validate_each(record, attribute, value)
- unless validate_array_of_strings(value)
- record.errors.add(attribute, 'should be an array of strings')
- end
- end
- end
-
- class BooleanValidator < ActiveModel::EachValidator
- include LegacyValidationHelpers
-
- def validate_each(record, attribute, value)
- unless validate_boolean(value)
- record.errors.add(attribute, 'should be a boolean value')
- end
- end
- end
-
- class DurationValidator < ActiveModel::EachValidator
- include LegacyValidationHelpers
-
- def validate_each(record, attribute, value)
- unless validate_duration(value)
- record.errors.add(attribute, 'should be a duration')
- end
- end
- end
-
- class KeyValidator < ActiveModel::EachValidator
- include LegacyValidationHelpers
-
- def validate_each(record, attribute, value)
- unless validate_string(value)
- record.errors.add(attribute, 'should be a string or symbol')
- end
- end
- end
-
- class TypeValidator < ActiveModel::EachValidator
- def validate_each(record, attribute, value)
- type = options[:with]
- raise unless type.is_a?(Class)
-
- unless value.is_a?(type)
- message = options[:message] || "should be a #{type.name}"
- record.errors.add(attribute, message)
- end
- end
- end
-
- class VariablesValidator < ActiveModel::EachValidator
- include LegacyValidationHelpers
-
- def validate_each(record, attribute, value)
- unless validate_variables(value)
- record.errors.add(attribute, 'should be a hash of key value pairs')
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/variables.rb b/lib/gitlab/ci/config/node/variables.rb
deleted file mode 100644
index 5f813f81f55..00000000000
--- a/lib/gitlab/ci/config/node/variables.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # Entry that represents environment variables.
- #
- class Variables < Entry
- include Validatable
-
- validations do
- validates :config, variables: true
- end
-
- def self.default
- {}
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb
new file mode 100644
index 00000000000..a979fe7d573
--- /dev/null
+++ b/lib/gitlab/ci/status/build/cancelable.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Cancelable < SimpleDelegator
+ include Status::Extended
+
+ def has_action?
+ can?(user, :update_build, subject)
+ end
+
+ def action_icon
+ 'ban'
+ end
+
+ def action_path
+ cancel_namespace_project_build_path(subject.project.namespace,
+ subject.project,
+ subject)
+ end
+
+ def action_method
+ :post
+ end
+
+ def action_title
+ 'Cancel'
+ end
+
+ def self.matches?(build, user)
+ build.cancelable?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb
new file mode 100644
index 00000000000..3fec2c5d4db
--- /dev/null
+++ b/lib/gitlab/ci/status/build/common.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ module Common
+ def has_details?
+ can?(user, :read_build, subject)
+ end
+
+ def details_path
+ namespace_project_build_path(subject.project.namespace,
+ subject.project,
+ subject)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb
new file mode 100644
index 00000000000..eee9a64120b
--- /dev/null
+++ b/lib/gitlab/ci/status/build/factory.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Factory < Status::Factory
+ def self.extended_statuses
+ [Status::Build::Stop, Status::Build::Play,
+ Status::Build::Cancelable, Status::Build::Retryable]
+ end
+
+ def self.common_helpers
+ Status::Build::Common
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb
new file mode 100644
index 00000000000..5c506e6d59f
--- /dev/null
+++ b/lib/gitlab/ci/status/build/play.rb
@@ -0,0 +1,53 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Play < SimpleDelegator
+ include Status::Extended
+
+ def text
+ 'manual'
+ end
+
+ def label
+ 'manual play action'
+ end
+
+ def icon
+ 'icon_status_manual'
+ end
+
+ def has_action?
+ can?(user, :update_build, subject)
+ end
+
+ def action_icon
+ '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,
+ subject)
+ end
+
+ def action_method
+ :post
+ end
+
+ def self.matches?(build, user)
+ build.playable? && !build.stops_environment?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb
new file mode 100644
index 00000000000..8e38d6a8523
--- /dev/null
+++ b/lib/gitlab/ci/status/build/retryable.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Retryable < SimpleDelegator
+ include Status::Extended
+
+ def has_action?
+ can?(user, :update_build, subject)
+ end
+
+ def action_icon
+ 'refresh'
+ end
+
+ def action_title
+ 'Retry'
+ end
+
+ def action_path
+ retry_namespace_project_build_path(subject.project.namespace,
+ subject.project,
+ subject)
+ end
+
+ def action_method
+ :post
+ end
+
+ def self.matches?(build, user)
+ build.retryable?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb
new file mode 100644
index 00000000000..f8ffa95cde4
--- /dev/null
+++ b/lib/gitlab/ci/status/build/stop.rb
@@ -0,0 +1,49 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Stop < SimpleDelegator
+ include Status::Extended
+
+ def text
+ 'manual'
+ end
+
+ def label
+ 'manual stop action'
+ end
+
+ def icon
+ 'icon_status_manual'
+ end
+
+ def has_action?
+ can?(user, :update_build, subject)
+ end
+
+ def action_icon
+ 'stop'
+ end
+
+ def action_title
+ 'Stop'
+ end
+
+ def action_path
+ play_namespace_project_build_path(subject.project.namespace,
+ subject.project,
+ subject)
+ end
+
+ def action_method
+ :post
+ end
+
+ def self.matches?(build, user)
+ build.playable? && build.stops_environment?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb
new file mode 100644
index 00000000000..dd6d99e9075
--- /dev/null
+++ b/lib/gitlab/ci/status/canceled.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Ci
+ module Status
+ class Canceled < Status::Core
+ def text
+ 'canceled'
+ end
+
+ def label
+ 'canceled'
+ end
+
+ def icon
+ 'icon_status_canceled'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb
new file mode 100644
index 00000000000..46fef8262c1
--- /dev/null
+++ b/lib/gitlab/ci/status/core.rb
@@ -0,0 +1,69 @@
+module Gitlab
+ module Ci
+ module Status
+ # Base abstract class fore core status
+ #
+ class Core
+ include Gitlab::Routing
+ include Gitlab::Allowable
+
+ attr_reader :subject, :user
+
+ def initialize(subject, user)
+ @subject = subject
+ @user = user
+ end
+
+ def icon
+ raise NotImplementedError
+ end
+
+ def label
+ raise NotImplementedError
+ end
+
+ # Deprecation warning: this method is here because we need to maintain
+ # backwards compatibility with legacy statuses. We often do something
+ # like "ci-status ci-status-#{status}" to set CSS class.
+ #
+ # `to_s` method should be renamed to `group` at some point, after
+ # phasing legacy satuses out.
+ #
+ def to_s
+ self.class.name.demodulize.downcase.underscore
+ end
+
+ def has_details?
+ false
+ end
+
+ def details_path
+ raise NotImplementedError
+ end
+
+ def has_action?
+ false
+ end
+
+ def action_icon
+ raise NotImplementedError
+ end
+
+ def action_class
+ end
+
+ def action_path
+ raise NotImplementedError
+ end
+
+ def action_method
+ raise NotImplementedError
+ end
+
+ def action_title
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb
new file mode 100644
index 00000000000..6596d7e01ca
--- /dev/null
+++ b/lib/gitlab/ci/status/created.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Ci
+ module Status
+ class Created < Status::Core
+ def text
+ 'created'
+ end
+
+ def label
+ 'created'
+ end
+
+ def icon
+ 'icon_status_created'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/extended.rb b/lib/gitlab/ci/status/extended.rb
new file mode 100644
index 00000000000..d367c9bda69
--- /dev/null
+++ b/lib/gitlab/ci/status/extended.rb
@@ -0,0 +1,15 @@
+module Gitlab
+ module Ci
+ module Status
+ module Extended
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def matches?(_subject, _user)
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb
new file mode 100644
index 00000000000..ae9ef895df4
--- /dev/null
+++ b/lib/gitlab/ci/status/factory.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ module Ci
+ module Status
+ class Factory
+ def initialize(subject, user)
+ @subject = subject
+ @user = user
+ end
+
+ def fabricate!
+ if extended_status
+ extended_status.new(core_status)
+ else
+ core_status
+ end
+ end
+
+ def self.extended_statuses
+ []
+ end
+
+ def self.common_helpers
+ Module.new
+ end
+
+ private
+
+ def simple_status
+ @simple_status ||= @subject.status || :created
+ end
+
+ def core_status
+ Gitlab::Ci::Status
+ .const_get(simple_status.capitalize)
+ .new(@subject, @user)
+ .extend(self.class.common_helpers)
+ end
+
+ def extended_status
+ @extended ||= self.class.extended_statuses.find do |status|
+ status.matches?(@subject, @user)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb
new file mode 100644
index 00000000000..c5b5e3203ad
--- /dev/null
+++ b/lib/gitlab/ci/status/failed.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Ci
+ module Status
+ class Failed < Status::Core
+ def text
+ 'failed'
+ end
+
+ def label
+ 'failed'
+ end
+
+ def icon
+ 'icon_status_failed'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb
new file mode 100644
index 00000000000..d30f35a59a2
--- /dev/null
+++ b/lib/gitlab/ci/status/pending.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Ci
+ module Status
+ class Pending < Status::Core
+ def text
+ 'pending'
+ end
+
+ def label
+ 'pending'
+ end
+
+ def icon
+ 'icon_status_pending'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/pipeline/common.rb b/lib/gitlab/ci/status/pipeline/common.rb
new file mode 100644
index 00000000000..76bfd18bf40
--- /dev/null
+++ b/lib/gitlab/ci/status/pipeline/common.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module Ci
+ module Status
+ module Pipeline
+ module Common
+ def has_details?
+ can?(user, :read_pipeline, subject)
+ end
+
+ def details_path
+ namespace_project_pipeline_path(subject.project.namespace,
+ subject.project,
+ subject)
+ end
+
+ def has_action?
+ false
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb
new file mode 100644
index 00000000000..16dcb326be9
--- /dev/null
+++ b/lib/gitlab/ci/status/pipeline/factory.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module Ci
+ module Status
+ module Pipeline
+ class Factory < Status::Factory
+ def self.extended_statuses
+ [Pipeline::SuccessWithWarnings]
+ end
+
+ def self.common_helpers
+ Status::Pipeline::Common
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb
new file mode 100644
index 00000000000..a7c98f9e909
--- /dev/null
+++ b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb
@@ -0,0 +1,31 @@
+module Gitlab
+ module Ci
+ module Status
+ module Pipeline
+ class SuccessWithWarnings < SimpleDelegator
+ include Status::Extended
+
+ def text
+ 'passed'
+ end
+
+ def label
+ 'passed with warnings'
+ end
+
+ def icon
+ 'icon_status_warning'
+ end
+
+ def to_s
+ 'success_with_warnings'
+ end
+
+ def self.matches?(pipeline, user)
+ pipeline.success? && pipeline.has_warnings?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb
new file mode 100644
index 00000000000..2aba3c373c7
--- /dev/null
+++ b/lib/gitlab/ci/status/running.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Ci
+ module Status
+ class Running < Status::Core
+ def text
+ 'running'
+ end
+
+ def label
+ 'running'
+ end
+
+ def icon
+ 'icon_status_running'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb
new file mode 100644
index 00000000000..16282aefd03
--- /dev/null
+++ b/lib/gitlab/ci/status/skipped.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Ci
+ module Status
+ class Skipped < Status::Core
+ def text
+ 'skipped'
+ end
+
+ def label
+ 'skipped'
+ end
+
+ def icon
+ 'icon_status_skipped'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb
new file mode 100644
index 00000000000..7852f492e1d
--- /dev/null
+++ b/lib/gitlab/ci/status/stage/common.rb
@@ -0,0 +1,24 @@
+module Gitlab
+ module Ci
+ module Status
+ module Stage
+ module Common
+ def has_details?
+ can?(user, :read_pipeline, subject.pipeline)
+ end
+
+ def details_path
+ namespace_project_pipeline_path(subject.project.namespace,
+ subject.project,
+ subject.pipeline,
+ anchor: subject.name)
+ end
+
+ def has_action?
+ false
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb
new file mode 100644
index 00000000000..689a5dd45bc
--- /dev/null
+++ b/lib/gitlab/ci/status/stage/factory.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module Ci
+ module Status
+ module Stage
+ class Factory < Status::Factory
+ def self.common_helpers
+ Status::Stage::Common
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb
new file mode 100644
index 00000000000..c09c5f006e3
--- /dev/null
+++ b/lib/gitlab/ci/status/success.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Ci
+ module Status
+ class Success < Status::Core
+ def text
+ 'passed'
+ end
+
+ def label
+ 'passed'
+ end
+
+ def icon
+ 'icon_status_success'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index b164f5a2eea..7e3d5647b39 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -1,45 +1,44 @@
module Gitlab
class ContributionsCalendar
- attr_reader :activity_dates, :projects, :user
+ attr_reader :contributor
+ attr_reader :current_user
+ attr_reader :projects
- def initialize(projects, user)
- @projects = projects
- @user = user
+ def initialize(contributor, current_user = nil)
+ @contributor = contributor
+ @current_user = current_user
+ @projects = ContributedProjectsFinder.new(contributor).execute(current_user)
end
def activity_dates
return @activity_dates if @activity_dates.present?
- @activity_dates = {}
+ # Can't use Event.contributions here because we need to check 3 different
+ # project_features for the (currently) 3 different contribution types
date_from = 1.year.ago
+ repo_events = event_counts(date_from, :repository).
+ having(action: Event::PUSHED)
+ issue_events = event_counts(date_from, :issues).
+ having(action: [Event::CREATED, Event::CLOSED], target_type: "Issue")
+ mr_events = event_counts(date_from, :merge_requests).
+ having(action: [Event::MERGED, Event::CREATED, Event::CLOSED], target_type: "MergeRequest")
- events = Event.reorder(nil).contributions.where(author_id: user.id).
- where("created_at > ?", date_from).where(project_id: projects).
- group('date(created_at)').
- select('date(created_at) as date, count(id) as total_amount').
- map(&:attributes)
+ union = Gitlab::SQL::Union.new([repo_events, issue_events, mr_events])
+ events = Event.find_by_sql(union.to_sql).map(&:attributes)
- activity_dates = (1.year.ago.to_date..Date.today).to_a
-
- activity_dates.each do |date|
- day_events = events.find { |day_events| day_events["date"] == date }
-
- if day_events
- @activity_dates[date] = day_events["total_amount"]
- end
+ @activity_events = events.each_with_object(Hash.new {|h, k| h[k] = 0 }) do |event, activities|
+ activities[event["date"]] += event["total_amount"]
end
-
- @activity_dates
end
def events_by_date(date)
- events = Event.contributions.where(author_id: user.id).
- where("created_at > ? AND created_at < ?", date.beginning_of_day, date.end_of_day).
+ events = Event.contributions.where(author_id: contributor.id).
+ where(created_at: date.beginning_of_day..date.end_of_day).
where(project_id: projects)
- events.select do |event|
- event.push? || event.issue? || event.merge_request?
- end
+ # Use visible_to_user? instead of the complicated logic in activity_dates
+ # because we're only viewing the events for a single day.
+ events.select {|event| event.visible_to_user?(current_user) }
end
def starting_year
@@ -49,5 +48,30 @@ module Gitlab
def starting_month
Date.today.month
end
+
+ private
+
+ def event_counts(date_from, feature)
+ t = Event.arel_table
+
+ # re-running the contributed projects query in each union is expensive, so
+ # use IN(project_ids...) instead. It's the intersection of two users so
+ # the list will be (relatively) short
+ @contributed_project_ids ||= projects.uniq.pluck(:id)
+ authed_projects = Project.where(id: @contributed_project_ids).
+ with_feature_available_for_user(feature, current_user).
+ reorder(nil).
+ select(:id)
+
+ conditions = t[:created_at].gteq(date_from.beginning_of_day).
+ and(t[:created_at].lteq(Date.today.end_of_day)).
+ and(t[:author_id].eq(contributor.id))
+
+ Event.reorder(nil).
+ select(t[:project_id], t[:target_type], t[:action], 'date(created_at) AS date', 'count(id) as total_amount').
+ group(t[:project_id], t[:target_type], t[:action], 'date(created_at)').
+ where(conditions).
+ having(t[:project_id].in(Arel::Nodes::SqlLiteral.new(authed_projects.to_sql)))
+ end
end
end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index ef9160d6437..c6bb8f9c8ed 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -23,6 +23,10 @@ module Gitlab
settings || fake_application_settings
end
+ def sidekiq_throttling_enabled?
+ current_application_settings.sidekiq_throttling_enabled?
+ end
+
def fake_application_settings
OpenStruct.new(
default_projects_limit: Settings.gitlab['default_projects_limit'],
@@ -50,6 +54,7 @@ module Gitlab
repository_checks_enabled: true,
container_registry_token_expire_delay: 5,
user_default_external: false,
+ sidekiq_throttling_enabled: false,
)
end
diff --git a/lib/gitlab/cycle_analytics/base_event.rb b/lib/gitlab/cycle_analytics/base_event.rb
new file mode 100644
index 00000000000..53a148ad703
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/base_event.rb
@@ -0,0 +1,57 @@
+module Gitlab
+ module CycleAnalytics
+ class BaseEvent
+ include MetricsTables
+
+ attr_reader :stage, :start_time_attrs, :end_time_attrs, :projections, :query
+
+ def initialize(project:, options:)
+ @query = EventsQuery.new(project: project, options: options)
+ @project = project
+ @options = options
+ end
+
+ def fetch
+ update_author!
+
+ event_result.map do |event|
+ serialize(event) if has_permission?(event['id'])
+ end.compact
+ end
+
+ def custom_query(_base_query); end
+
+ def order
+ @order || @start_time_attrs
+ end
+
+ private
+
+ def update_author!
+ return unless event_result.any? && event_result.first['author_id']
+
+ Updater.update!(event_result, from: 'author_id', to: 'author', klass: User)
+ end
+
+ def event_result
+ @event_result ||= @query.execute(self).to_a
+ end
+
+ def serialize(_event)
+ raise NotImplementedError.new("Expected #{self.name} to implement serialize(event)")
+ end
+
+ def has_permission?(id)
+ allowed_ids.nil? || allowed_ids.include?(id.to_i)
+ end
+
+ def allowed_ids
+ nil
+ end
+
+ def event_result_ids
+ event_result.map { |event| event['id'] }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/code_event.rb b/lib/gitlab/cycle_analytics/code_event.rb
new file mode 100644
index 00000000000..2afdf0b8518
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/code_event.rb
@@ -0,0 +1,28 @@
+module Gitlab
+ module CycleAnalytics
+ class CodeEvent < BaseEvent
+ include MergeRequestAllowed
+
+ def initialize(*args)
+ @stage = :code
+ @start_time_attrs = issue_metrics_table[:first_mentioned_in_commit_at]
+ @end_time_attrs = mr_table[:created_at]
+ @projections = [mr_table[:title],
+ mr_table[:iid],
+ mr_table[:id],
+ mr_table[:created_at],
+ mr_table[:state],
+ mr_table[:author_id]]
+ @order = mr_table[:created_at]
+
+ super(*args)
+ end
+
+ private
+
+ def serialize(event)
+ AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/events.rb b/lib/gitlab/cycle_analytics/events.rb
new file mode 100644
index 00000000000..2d703d76cbb
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/events.rb
@@ -0,0 +1,38 @@
+module Gitlab
+ module CycleAnalytics
+ class Events
+ def initialize(project:, options:)
+ @project = project
+ @options = options
+ end
+
+ def issue_events
+ IssueEvent.new(project: @project, options: @options).fetch
+ end
+
+ def plan_events
+ PlanEvent.new(project: @project, options: @options).fetch
+ end
+
+ def code_events
+ CodeEvent.new(project: @project, options: @options).fetch
+ end
+
+ def test_events
+ TestEvent.new(project: @project, options: @options).fetch
+ end
+
+ def review_events
+ ReviewEvent.new(project: @project, options: @options).fetch
+ end
+
+ def staging_events
+ StagingEvent.new(project: @project, options: @options).fetch
+ end
+
+ def production_events
+ ProductionEvent.new(project: @project, options: @options).fetch
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/events_query.rb b/lib/gitlab/cycle_analytics/events_query.rb
new file mode 100644
index 00000000000..2418832ccc2
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/events_query.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module CycleAnalytics
+ class EventsQuery
+ attr_reader :project
+
+ def initialize(project:, options: {})
+ @project = project
+ @from = options[:from]
+ @branch = options[:branch]
+ @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: @from, branch: @branch)
+ end
+
+ def execute(stage_class)
+ @stage_class = stage_class
+
+ ActiveRecord::Base.connection.exec_query(query.to_sql)
+ end
+
+ private
+
+ def query
+ base_query = @fetcher.base_query_for(@stage_class.stage)
+ diff_fn = @fetcher.subtract_datetimes_diff(base_query, @stage_class.start_time_attrs, @stage_class.end_time_attrs)
+
+ @stage_class.custom_query(base_query)
+
+ base_query.project(extract_epoch(diff_fn).as('total_time'), *@stage_class.projections).order(@stage_class.order.desc)
+ end
+
+ def extract_epoch(arel_attribute)
+ return arel_attribute unless Gitlab::Database.postgresql?
+
+ Arel.sql(%Q{EXTRACT(EPOCH FROM (#{arel_attribute.to_sql}))})
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/issue_allowed.rb b/lib/gitlab/cycle_analytics/issue_allowed.rb
new file mode 100644
index 00000000000..a7652a70641
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/issue_allowed.rb
@@ -0,0 +1,9 @@
+module Gitlab
+ module CycleAnalytics
+ module IssueAllowed
+ def allowed_ids
+ @allowed_ids ||= IssuesFinder.new(@options[:current_user], project_id: @project.id).execute.where(id: event_result_ids).pluck(:id)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/issue_event.rb b/lib/gitlab/cycle_analytics/issue_event.rb
new file mode 100644
index 00000000000..705b7e5ce24
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/issue_event.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module CycleAnalytics
+ class IssueEvent < BaseEvent
+ include IssueAllowed
+
+ def initialize(*args)
+ @stage = :issue
+ @start_time_attrs = issue_table[:created_at]
+ @end_time_attrs = [issue_metrics_table[:first_associated_with_milestone_at],
+ issue_metrics_table[:first_added_to_board_at]]
+ @projections = [issue_table[:title],
+ issue_table[:iid],
+ issue_table[:id],
+ issue_table[:created_at],
+ issue_table[:author_id]]
+
+ super(*args)
+ end
+
+ private
+
+ def serialize(event)
+ AnalyticsIssueSerializer.new(project: @project).represent(event).as_json
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/merge_request_allowed.rb b/lib/gitlab/cycle_analytics/merge_request_allowed.rb
new file mode 100644
index 00000000000..28f6db44759
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/merge_request_allowed.rb
@@ -0,0 +1,9 @@
+module Gitlab
+ module CycleAnalytics
+ module MergeRequestAllowed
+ def allowed_ids
+ @allowed_ids ||= MergeRequestsFinder.new(@options[:current_user], project_id: @project.id).execute.where(id: event_result_ids).pluck(:id)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/metrics_fetcher.rb b/lib/gitlab/cycle_analytics/metrics_fetcher.rb
new file mode 100644
index 00000000000..b71e8735e27
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/metrics_fetcher.rb
@@ -0,0 +1,60 @@
+module Gitlab
+ module CycleAnalytics
+ class MetricsFetcher
+ include Gitlab::Database::Median
+ include Gitlab::Database::DateTime
+ include MetricsTables
+
+ DEPLOYMENT_METRIC_STAGES = %i[production staging]
+
+ def initialize(project:, from:, branch:)
+ @project = project
+ @project = project
+ @from = from
+ @branch = branch
+ end
+
+ def calculate_metric(name, start_time_attrs, end_time_attrs)
+ cte_table = Arel::Table.new("cte_table_for_#{name}")
+
+ # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
+ # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
+ # We compute the (end_time - start_time) interval, and give it an alias based on the current
+ # cycle analytics stage.
+ interval_query = Arel::Nodes::As.new(
+ cte_table,
+ subtract_datetimes(base_query_for(name), start_time_attrs, end_time_attrs, name.to_s))
+
+ median_datetime(cte_table, interval_query, name)
+ end
+
+ # Join table with a row for every <issue,merge_request> pair (where the merge request
+ # closes the given issue) with issue and merge request metrics included. The metrics
+ # are loaded with an inner join, so issues / merge requests without metrics are
+ # automatically excluded.
+ def base_query_for(name)
+ # Load issues
+ query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])).
+ join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])).
+ where(issue_table[:project_id].eq(@project.id)).
+ where(issue_table[:deleted_at].eq(nil)).
+ where(issue_table[:created_at].gteq(@from))
+
+ query = query.where(build_table[:ref].eq(@branch)) if name == :test && @branch
+
+ # Load merge_requests
+ query = query.join(mr_table, Arel::Nodes::OuterJoin).
+ on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id])).
+ join(mr_metrics_table).
+ on(mr_table[:id].eq(mr_metrics_table[:merge_request_id]))
+
+ if DEPLOYMENT_METRIC_STAGES.include?(name)
+ # Limit to merge requests that have been deployed to production after `@from`
+ query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from))
+ end
+
+ query
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/metrics_tables.rb b/lib/gitlab/cycle_analytics/metrics_tables.rb
new file mode 100644
index 00000000000..9d25ef078e8
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/metrics_tables.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module CycleAnalytics
+ module MetricsTables
+ def mr_metrics_table
+ MergeRequest::Metrics.arel_table
+ end
+
+ def mr_table
+ MergeRequest.arel_table
+ end
+
+ def mr_diff_table
+ MergeRequestDiff.arel_table
+ end
+
+ def mr_closing_issues_table
+ MergeRequestsClosingIssues.arel_table
+ end
+
+ def issue_table
+ Issue.arel_table
+ end
+
+ def issue_metrics_table
+ Issue::Metrics.arel_table
+ end
+
+ def user_table
+ User.arel_table
+ end
+
+ def build_table
+ ::CommitStatus.arel_table
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/permissions.rb b/lib/gitlab/cycle_analytics/permissions.rb
new file mode 100644
index 00000000000..bef3b95ff1b
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/permissions.rb
@@ -0,0 +1,44 @@
+module Gitlab
+ module CycleAnalytics
+ class Permissions
+ STAGE_PERMISSIONS = {
+ issue: :read_issue,
+ code: :read_merge_request,
+ test: :read_build,
+ review: :read_merge_request,
+ staging: :read_build,
+ production: :read_issue,
+ }.freeze
+
+ def self.get(*args)
+ new(*args).get
+ end
+
+ def initialize(user:, project:)
+ @user = user
+ @project = project
+ @stage_permission_hash = {}
+ end
+
+ def get
+ ::CycleAnalytics::STAGES.each do |stage|
+ @stage_permission_hash[stage] = authorized_stage?(stage)
+ end
+
+ @stage_permission_hash
+ end
+
+ private
+
+ def authorized_stage?(stage)
+ return false unless authorize_project(:read_cycle_analytics)
+
+ STAGE_PERMISSIONS[stage] ? authorize_project(STAGE_PERMISSIONS[stage]) : true
+ end
+
+ def authorize_project(permission)
+ Ability.allowed?(@user, permission, @project)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/plan_event.rb b/lib/gitlab/cycle_analytics/plan_event.rb
new file mode 100644
index 00000000000..7c3f0e9989f
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/plan_event.rb
@@ -0,0 +1,46 @@
+module Gitlab
+ module CycleAnalytics
+ class PlanEvent < BaseEvent
+ def initialize(*args)
+ @stage = :plan
+ @start_time_attrs = issue_metrics_table[:first_associated_with_milestone_at]
+ @end_time_attrs = [issue_metrics_table[:first_added_to_board_at],
+ issue_metrics_table[:first_mentioned_in_commit_at]]
+ @projections = [mr_diff_table[:st_commits].as('commits'),
+ issue_metrics_table[:first_mentioned_in_commit_at]]
+
+ super(*args)
+ end
+
+ def custom_query(base_query)
+ base_query.join(mr_diff_table).on(mr_diff_table[:merge_request_id].eq(mr_table[:id]))
+ end
+
+ private
+
+ def serialize(event)
+ st_commit = first_time_reference_commit(event.delete('commits'), event)
+
+ return unless st_commit
+
+ serialize_commit(event, st_commit, query)
+ end
+
+ def first_time_reference_commit(commits, event)
+ return nil if commits.blank?
+
+ YAML.load(commits).find do |commit|
+ next unless commit[:committed_date] && event['first_mentioned_in_commit_at']
+
+ commit[:committed_date].to_i == DateTime.parse(event['first_mentioned_in_commit_at'].to_s).to_i
+ end
+ end
+
+ def serialize_commit(event, st_commit, query)
+ commit = Commit.new(Gitlab::Git::Commit.new(st_commit), @project)
+
+ AnalyticsCommitSerializer.new(project: @project, total_time: event['total_time']).represent(commit).as_json
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/production_event.rb b/lib/gitlab/cycle_analytics/production_event.rb
new file mode 100644
index 00000000000..4868c3c6237
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/production_event.rb
@@ -0,0 +1,26 @@
+module Gitlab
+ module CycleAnalytics
+ class ProductionEvent < BaseEvent
+ include IssueAllowed
+
+ def initialize(*args)
+ @stage = :production
+ @start_time_attrs = issue_table[:created_at]
+ @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at]
+ @projections = [issue_table[:title],
+ issue_table[:iid],
+ issue_table[:id],
+ issue_table[:created_at],
+ issue_table[:author_id]]
+
+ super(*args)
+ end
+
+ private
+
+ def serialize(event)
+ AnalyticsIssueSerializer.new(project: @project).represent(event).as_json
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/review_event.rb b/lib/gitlab/cycle_analytics/review_event.rb
new file mode 100644
index 00000000000..b394a02cc52
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/review_event.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module CycleAnalytics
+ class ReviewEvent < BaseEvent
+ include MergeRequestAllowed
+
+ def initialize(*args)
+ @stage = :review
+ @start_time_attrs = mr_table[:created_at]
+ @end_time_attrs = mr_metrics_table[:merged_at]
+ @projections = [mr_table[:title],
+ mr_table[:iid],
+ mr_table[:id],
+ mr_table[:created_at],
+ mr_table[:state],
+ mr_table[:author_id]]
+
+ super(*args)
+ end
+
+ def serialize(event)
+ AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/staging_event.rb b/lib/gitlab/cycle_analytics/staging_event.rb
new file mode 100644
index 00000000000..a1f30b716f6
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/staging_event.rb
@@ -0,0 +1,31 @@
+module Gitlab
+ module CycleAnalytics
+ class StagingEvent < BaseEvent
+ def initialize(*args)
+ @stage = :staging
+ @start_time_attrs = mr_metrics_table[:merged_at]
+ @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at]
+ @projections = [build_table[:id]]
+ @order = build_table[:created_at]
+
+ super(*args)
+ end
+
+ def fetch
+ Updater.update!(event_result, from: 'id', to: 'build', klass: ::Ci::Build)
+
+ super
+ end
+
+ def custom_query(base_query)
+ base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id]))
+ end
+
+ private
+
+ def serialize(event)
+ AnalyticsBuildSerializer.new.represent(event['build']).as_json
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/test_event.rb b/lib/gitlab/cycle_analytics/test_event.rb
new file mode 100644
index 00000000000..d553d0b5aec
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/test_event.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module CycleAnalytics
+ class TestEvent < StagingEvent
+ def initialize(*args)
+ super(*args)
+
+ @stage = :test
+ @start_time_attrs = mr_metrics_table[:latest_build_started_at]
+ @end_time_attrs = mr_metrics_table[:latest_build_finished_at]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/updater.rb b/lib/gitlab/cycle_analytics/updater.rb
new file mode 100644
index 00000000000..953268ebd46
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/updater.rb
@@ -0,0 +1,30 @@
+module Gitlab
+ module CycleAnalytics
+ class Updater
+ def self.update!(*args)
+ new(*args).update!
+ end
+
+ def initialize(event_result, from:, to:, klass:)
+ @event_result = event_result
+ @klass = klass
+ @from = from
+ @to = to
+ end
+
+ def update!
+ @event_result.each do |event|
+ event[@to] = items[event.delete(@from).to_i].first
+ end
+ end
+
+ def result_ids
+ @event_result.map { |event| event[@from] }
+ end
+
+ def items
+ @items ||= @klass.find(result_ids).group_by { |item| item['id'] }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb
index 06a783ebc1c..e50e54b6e99 100644
--- a/lib/gitlab/data_builder/pipeline.rb
+++ b/lib/gitlab/data_builder/pipeline.rb
@@ -22,7 +22,7 @@ module Gitlab
sha: pipeline.sha,
before_sha: pipeline.before_sha,
status: pipeline.status,
- stages: pipeline.stages,
+ stages: pipeline.stages_name,
created_at: pipeline.created_at,
finished_at: pipeline.finished_at,
duration: pipeline.duration
diff --git a/lib/gitlab/database/date_time.rb b/lib/gitlab/database/date_time.rb
index b6a89f715fd..25e56998038 100644
--- a/lib/gitlab/database/date_time.rb
+++ b/lib/gitlab/database/date_time.rb
@@ -7,21 +7,25 @@ module Gitlab
#
# Note: For MySQL, the interval is returned in seconds.
# For PostgreSQL, the interval is returned as an INTERVAL type.
- def subtract_datetimes(query_so_far, end_time_attrs, start_time_attrs, as)
- diff_fn = if Gitlab::Database.postgresql?
- Arel::Nodes::Subtraction.new(
- Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs)),
- Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)))
- elsif Gitlab::Database.mysql?
- Arel::Nodes::NamedFunction.new(
- "TIMESTAMPDIFF",
- [Arel.sql('second'),
- Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)),
- Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs))])
- end
+ def subtract_datetimes(query_so_far, start_time_attrs, end_time_attrs, as)
+ diff_fn = subtract_datetimes_diff(query_so_far, start_time_attrs, end_time_attrs)
query_so_far.project(diff_fn.as(as))
end
+
+ def subtract_datetimes_diff(query_so_far, start_time_attrs, end_time_attrs)
+ if Gitlab::Database.postgresql?
+ Arel::Nodes::Subtraction.new(
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs)),
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)))
+ elsif Gitlab::Database.mysql?
+ Arel::Nodes::NamedFunction.new(
+ "TIMESTAMPDIFF",
+ [Arel.sql('second'),
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)),
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs))])
+ end
+ end
end
end
end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index ce85e5e0123..c6bf25b5874 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -55,6 +55,12 @@ module Gitlab
repository.commit(deleted_file ? old_ref : new_ref)
end
+ def old_content_commit
+ return unless diff_refs
+
+ repository.commit(old_ref)
+ end
+
def old_ref
diff_refs.try(:base_sha)
end
@@ -111,13 +117,10 @@ module Gitlab
diff_lines.count(&:removed?)
end
- def old_blob(commit = content_commit)
+ def old_blob(commit = old_content_commit)
return unless commit
- parent_id = commit.parent_id
- return unless parent_id
-
- repository.blob_at(parent_id, old_path)
+ repository.blob_at(commit.id, old_path)
end
def blob(commit = content_commit)
@@ -126,7 +129,7 @@ module Gitlab
repository.blob_at(commit.id, file_path)
end
- def cache_key
+ def file_identifier
"#{file_path}-#{new_file}-#{deleted_file}-#{renamed_file}"
end
end
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb
index dc4d47c878b..56530448f36 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb
@@ -20,7 +20,7 @@ module Gitlab
# Extracted method to highlight in the same iteration to the diff_collection.
def decorate_diff!(diff)
diff_file = super
- cache_highlight!(diff_file) if cacheable?
+ cache_highlight!(diff_file) if cacheable?(diff_file)
diff_file
end
@@ -39,7 +39,7 @@ module Gitlab
# hashes that represent serialized diff lines.
#
def cache_highlight!(diff_file)
- item_key = diff_file.cache_key
+ item_key = diff_file.file_identifier
if highlight_cache[item_key]
highlight_diff_file_from_cache!(diff_file, highlight_cache[item_key])
@@ -60,8 +60,8 @@ module Gitlab
Rails.cache.write(cache_key, highlight_cache) if @highlight_cache_was_empty
end
- def cacheable?
- @merge_request_diff.present?
+ def cacheable?(diff_file)
+ @merge_request_diff.present? && diff_file.blob && diff_file.blob.text?
end
def cache_key
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index b1a6d5fe0f6..c8e36d8ff4a 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -2,39 +2,38 @@
module Gitlab
# Checks if a set of migrations requires downtime or not.
class EeCompatCheck
+ CE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ce.git'.freeze
EE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
+ CHECK_DIR = Rails.root.join('ee_compat_check')
+ MAX_FETCH_DEPTH = 500
+ IGNORED_FILES_REGEX = /(VERSION|CHANGELOG\.md:\d+)/.freeze
- attr_reader :ce_branch, :check_dir, :ce_repo
+ attr_reader :repo_dir, :patches_dir, :ce_repo, :ce_branch
- def initialize(branch:, check_dir:, ce_repo: nil)
+ def initialize(branch:, ce_repo: CE_REPO)
+ @repo_dir = CHECK_DIR.join('repo')
+ @patches_dir = CHECK_DIR.join('patches')
@ce_branch = branch
- @check_dir = check_dir
- @ce_repo = ce_repo || 'https://gitlab.com/gitlab-org/gitlab-ce.git'
+ @ce_repo = ce_repo
end
def check
ensure_ee_repo
- delete_patches
+ ensure_patches_dir
generate_patch(ce_branch, ce_patch_full_path)
- Dir.chdir(check_dir) do
- step("In the #{check_dir} directory")
-
- step("Pulling latest master", %w[git pull --ff-only origin master])
+ Dir.chdir(repo_dir) do
+ step("In the #{repo_dir} directory")
status = catch(:halt_check) do
ce_branch_compat_check!
-
- delete_ee_branch_locally
-
+ delete_ee_branch_locally!
ee_branch_presence_check!
-
ee_branch_compat_check!
end
- delete_ee_branch_locally
- delete_patches
+ delete_ee_branch_locally!
if status.nil?
true
@@ -47,20 +46,43 @@ module Gitlab
private
def ensure_ee_repo
- if Dir.exist?(check_dir)
- step("#{check_dir} already exists")
+ if Dir.exist?(repo_dir)
+ step("#{repo_dir} already exists")
else
- cmd = %W[git clone --branch master --single-branch --depth 1 #{EE_REPO} #{check_dir}]
- step("Cloning #{EE_REPO} into #{check_dir}", cmd)
+ cmd = %W[git clone --branch master --single-branch --depth 200 #{EE_REPO} #{repo_dir}]
+ step("Cloning #{EE_REPO} into #{repo_dir}", cmd)
end
end
- def ce_branch_compat_check!
- cmd = %W[git apply --check #{ce_patch_full_path}]
- status = step("Checking if #{ce_patch_name} applies cleanly to EE/master", cmd)
+ def ensure_patches_dir
+ FileUtils.mkdir_p(patches_dir)
+ end
+
+ def generate_patch(branch, patch_path)
+ FileUtils.rm(patch_path, force: true)
+
+ depth = 0
+ loop do
+ depth += 50
+ cmd = %W[git fetch --depth #{depth} origin --prune +refs/heads/master:refs/remotes/origin/master]
+ Gitlab::Popen.popen(cmd)
+ _, status = Gitlab::Popen.popen(%w[git merge-base FETCH_HEAD HEAD])
+
+ raise "#{branch} is too far behind master, please rebase it!" if depth >= MAX_FETCH_DEPTH
+ break if status.zero?
+ end
- if status.zero?
- puts ce_applies_cleanly_msg(ce_branch)
+ step("Generating the patch against master in #{patch_path}")
+ output, status = Gitlab::Popen.popen(%w[git format-patch FETCH_HEAD --stdout])
+ throw(:halt_check, :ko) unless status.zero?
+
+ File.write(patch_path, output)
+ throw(:halt_check, :ko) unless File.exist?(patch_path)
+ end
+
+ def ce_branch_compat_check!
+ if check_patch(ce_patch_full_path).zero?
+ puts applies_cleanly_msg(ce_branch)
throw(:halt_check)
end
end
@@ -80,10 +102,8 @@ module Gitlab
step("Checking out origin/#{ee_branch}", %W[git checkout -b #{ee_branch} FETCH_HEAD])
generate_patch(ee_branch, ee_patch_full_path)
- cmd = %W[git apply --check #{ee_patch_full_path}]
- status = step("Checking if #{ee_patch_name} applies cleanly to EE/master", cmd)
- unless status.zero?
+ unless check_patch(ee_patch_full_path).zero?
puts
puts ee_branch_doesnt_apply_cleanly_msg
@@ -91,50 +111,49 @@ module Gitlab
end
puts
- puts ee_applies_cleanly_msg
+ puts applies_cleanly_msg(ee_branch)
end
- def generate_patch(branch, filepath)
- FileUtils.rm(filepath, force: true)
-
- depth = 0
- loop do
- depth += 10
- step("Fetching origin/master", %W[git fetch origin master --depth=#{depth}])
- status = step("Finding merge base with master", %W[git merge-base FETCH_HEAD #{branch}])
+ def check_patch(patch_path)
+ step("Checking out master", %w[git checkout master])
+ step("Reseting to latest master", %w[git reset --hard origin/master])
- break if status.zero? || depth > 500
- end
+ step("Checking if #{patch_path} applies cleanly to EE/master")
+ output, status = Gitlab::Popen.popen(%W[git apply --check #{patch_path}])
- raise "#{branch} is too far behind master, please rebase it!" if depth > 500
+ unless status.zero?
+ failed_files = output.lines.reduce([]) do |memo, line|
+ if line.start_with?('error: patch failed:')
+ file = line.sub(/\Aerror: patch failed: /, '')
+ memo << file unless file =~ IGNORED_FILES_REGEX
+ end
+ memo
+ end
- step("Generating the patch against master")
- output, status = Gitlab::Popen.popen(%w[git format-patch FETCH_HEAD --stdout])
- throw(:halt_check, :ko) unless status.zero?
+ if failed_files.empty?
+ status = 0
+ else
+ puts "\nConflicting files:"
+ failed_files.each do |file|
+ puts " - #{file}"
+ end
+ end
+ end
- File.write(filepath, output)
- throw(:halt_check, :ko) unless File.exist?(filepath)
+ status
end
- def delete_ee_branch_locally
+ def delete_ee_branch_locally!
command(%w[git checkout master])
step("Deleting the local #{ee_branch} branch", %W[git branch -D #{ee_branch}])
end
- def delete_patches
- step("Deleting #{ce_patch_full_path}")
- FileUtils.rm(ce_patch_full_path, force: true)
-
- step("Deleting #{ee_patch_full_path}")
- FileUtils.rm(ee_patch_full_path, force: true)
- end
-
def ce_patch_name
- @ce_patch_name ||= "#{ce_branch}.patch"
+ @ce_patch_name ||= patch_name_from_branch(ce_branch)
end
def ce_patch_full_path
- @ce_patch_full_path ||= File.expand_path(ce_patch_name, check_dir)
+ @ce_patch_full_path ||= patches_dir.join(ce_patch_name)
end
def ee_branch
@@ -142,19 +161,26 @@ module Gitlab
end
def ee_patch_name
- @ee_patch_name ||= "#{ee_branch}.patch"
+ @ee_patch_name ||= patch_name_from_branch(ee_branch)
end
def ee_patch_full_path
- @ee_patch_full_path ||= File.expand_path(ee_patch_name, check_dir)
+ @ee_patch_full_path ||= patches_dir.join(ee_patch_name)
+ end
+
+ def patch_name_from_branch(branch_name)
+ branch_name.parameterize << '.patch'
end
def step(desc, cmd = nil)
puts "\n=> #{desc}\n"
if cmd
+ start = Time.now
puts "\n$ #{cmd.join(' ')}"
- command(cmd)
+ status = command(cmd)
+ puts "\nFinished in #{Time.now - start} seconds"
+ status
end
end
@@ -165,12 +191,12 @@ module Gitlab
status
end
- def ce_applies_cleanly_msg(ce_branch)
+ def applies_cleanly_msg(branch)
<<-MSG.strip_heredoc
=================================================================
🎉 Congratulations!! 🎉
- The #{ce_branch} branch applies cleanly to EE/master!
+ The #{branch} branch applies cleanly to EE/master!
Much ❤️!!
=================================================================\n
@@ -211,7 +237,7 @@ module Gitlab
# In the EE repo
$ git fetch origin
- $ git checkout -b #{ee_branch} FETCH_HEAD
+ $ git checkout -b #{ee_branch} origin/master
$ git fetch #{ce_repo} #{ce_branch}
$ git cherry-pick SHA # Repeat for all the commits you want to pick
@@ -245,17 +271,5 @@ module Gitlab
=================================================================\n
MSG
end
-
- def ee_applies_cleanly_msg
- <<-MSG.strip_heredoc
- =================================================================
- 🎉 Congratulations!! 🎉
-
- The #{ee_branch} branch applies cleanly to EE/master!
-
- Much ❤️!!
- =================================================================\n
- MSG
- end
end
end
diff --git a/lib/gitlab/email/html_parser.rb b/lib/gitlab/email/html_parser.rb
new file mode 100644
index 00000000000..a4ca62bfc41
--- /dev/null
+++ b/lib/gitlab/email/html_parser.rb
@@ -0,0 +1,34 @@
+module Gitlab
+ module Email
+ class HTMLParser
+ def self.parse_reply(raw_body)
+ new(raw_body).filtered_text
+ end
+
+ attr_reader :raw_body
+ def initialize(raw_body)
+ @raw_body = raw_body
+ end
+
+ def document
+ @document ||= Nokogiri::HTML.parse(raw_body)
+ end
+
+ def filter_replies!
+ document.xpath('//blockquote').each(&:remove)
+ document.xpath('//table').each(&:remove)
+ end
+
+ def filtered_html
+ @filtered_html ||= begin
+ filter_replies!
+ document.inner_html
+ end
+ end
+
+ def filtered_text
+ @filtered_text ||= Html2Text.convert(filtered_html)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb
index 3411eb1d9ce..f586c5ab062 100644
--- a/lib/gitlab/email/reply_parser.rb
+++ b/lib/gitlab/email/reply_parser.rb
@@ -23,19 +23,26 @@ module Gitlab
private
def select_body(message)
- text = message.text_part if message.multipart?
- text ||= message if message.content_type !~ /text\/html/
+ if message.multipart?
+ part = message.text_part || message.html_part || message
+ else
+ part = message
+ end
- return "" unless text
+ decoded = fix_charset(part)
- text = fix_charset(text)
+ return "" unless decoded
# Certain trigger phrases that means we didn't parse correctly
- if text =~ /(Content\-Type\:|multipart\/alternative|text\/plain)/
+ if decoded =~ /(Content\-Type\:|multipart\/alternative|text\/plain)/
return ""
end
- text
+ if (part.content_type || '').include? 'text/html'
+ HTMLParser.parse_reply(decoded)
+ else
+ decoded
+ end
end
# Force encoding to UTF-8 on a Mail::Message or Mail::Part
@@ -62,7 +69,7 @@ module Gitlab
# This one might be controversial but so many reply lines have years, times and end with a colon.
# Let's try it and see how well it works.
break if (l =~ /\d{4}/ && l =~ /\d:\d\d/ && l =~ /\:$/) ||
- (l =~ /On \w+ \d+,? \d+,?.*wrote:/)
+ (l =~ /On \w+ \d+,? \d+,?.*wrote:/)
# Headers on subsequent lines
break if (0..2).all? { |off| lines[idx + off] =~ REPLYING_HEADER_REGEX }
diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb
new file mode 100644
index 00000000000..1d93a67dc56
--- /dev/null
+++ b/lib/gitlab/file_detector.rb
@@ -0,0 +1,63 @@
+require 'set'
+
+module Gitlab
+ # Module that can be used to detect if a path points to a special file such as
+ # a README or a CONTRIBUTING file.
+ module FileDetector
+ PATTERNS = {
+ readme: /\Areadme/i,
+ changelog: /\A(changelog|history|changes|news)/i,
+ license: /\A(licen[sc]e|copying)(\..+|\z)/i,
+ contributing: /\Acontributing/i,
+ version: 'version',
+ gitignore: '.gitignore',
+ koding: '.koding.yml',
+ gitlab_ci: '.gitlab-ci.yml',
+ avatar: /\Alogo\.(png|jpg|gif)\z/
+ }
+
+ # Returns an Array of file types based on the given paths.
+ #
+ # This method can be used to check if a list of file paths (e.g. of changed
+ # files) involve any special files such as a README or a LICENSE file.
+ #
+ # Example:
+ #
+ # types_in_paths(%w{README.md foo/bar.txt}) # => [:readme]
+ def self.types_in_paths(paths)
+ types = Set.new
+
+ paths.each do |path|
+ type = type_of(path)
+
+ types << type if type
+ end
+
+ types.to_a
+ end
+
+ # Returns the type of a file path, or nil if none could be detected.
+ #
+ # Returned types are Symbols such as `:readme`, `:version`, etc.
+ #
+ # Example:
+ #
+ # type_of('README.md') # => :readme
+ # type_of('VERSION') # => :version
+ def self.type_of(path)
+ name = File.basename(path)
+
+ PATTERNS.each do |type, search|
+ did_match = if search.is_a?(Regexp)
+ name =~ search
+ else
+ name.casecmp(search) == 0
+ end
+
+ return type if did_match
+ end
+
+ nil
+ end
+ end
+end
diff --git a/lib/gitlab/gfm/uploads_rewriter.rb b/lib/gitlab/gfm/uploads_rewriter.rb
index abc8c8c55e6..8fab5489616 100644
--- a/lib/gitlab/gfm/uploads_rewriter.rb
+++ b/lib/gitlab/gfm/uploads_rewriter.rb
@@ -1,3 +1,5 @@
+require 'fileutils'
+
module Gitlab
module Gfm
##
@@ -22,7 +24,9 @@ module Gitlab
return markdown unless file.try(:exists?)
new_uploader = FileUploader.new(target_project)
- new_uploader.store!(file)
+ with_link_in_tmp_dir(file.file) do |open_tmp_file|
+ new_uploader.store!(open_tmp_file)
+ end
new_uploader.to_markdown
end
end
@@ -46,6 +50,19 @@ module Gitlab
uploader.retrieve_from_store!(file)
uploader.file
end
+
+ # Because the uploaders use 'move_to_store' we must have a temporary
+ # file that is allowed to be (re)moved.
+ def with_link_in_tmp_dir(file)
+ dir = Dir.mktmpdir('UploadsRewriter', File.dirname(file))
+ # The filename matters to Carrierwave so we make sure to preserve it
+ tmp_file = File.join(dir, File.basename(file))
+ File.link(file, tmp_file)
+ # Open the file to placate Carrierwave
+ File.open(tmp_file) { |open_file| yield open_file }
+ ensure
+ FileUtils.rm_rf(dir)
+ end
end
end
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 799794c0171..db07b7c5fcc 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -2,8 +2,18 @@
# class return an instance of `GitlabAccessStatus`
module Gitlab
class GitAccess
+ UnauthorizedError = Class.new(StandardError)
+
+ ERROR_MESSAGES = {
+ upload: 'You are not allowed to upload code for this project.',
+ download: 'You are not allowed to download code from this project.',
+ deploy_key: 'Deploy keys are not allowed to push code.',
+ no_repo: 'A repository for this project does not exist yet.'
+ }
+
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }
PUSH_COMMANDS = %w{ git-receive-pack }
+ ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities
@@ -16,56 +26,47 @@ module Gitlab
end
def check(cmd, changes)
- return build_status_object(false, "Git access over #{protocol.upcase} is not allowed") unless protocol_allowed?
-
- unless actor
- return build_status_object(false, "No user or key was provided.")
- end
-
- if user && !user_access.allowed?
- return build_status_object(false, "Your account has been blocked.")
- end
-
- unless project && (user_access.can_read_project? || deploy_key_can_read_project?)
- return build_status_object(false, 'The project you were looking for could not be found.')
- end
+ check_protocol!
+ check_active_user!
+ check_project_accessibility!
+ check_command_existence!(cmd)
case cmd
when *DOWNLOAD_COMMANDS
download_access_check
when *PUSH_COMMANDS
push_access_check(changes)
- else
- build_status_object(false, "The command you're trying to execute is not allowed.")
end
+
+ build_status_object(true)
+ rescue UnauthorizedError => ex
+ build_status_object(false, ex.message)
end
def download_access_check
if user
user_download_access_check
- elsif deploy_key
- build_status_object(true)
- else
- raise 'Wrong actor'
+ elsif deploy_key.nil? && !guest_can_downlod_code?
+ raise UnauthorizedError, ERROR_MESSAGES[:download]
end
end
def push_access_check(changes)
if user
user_push_access_check(changes)
- elsif deploy_key
- build_status_object(false, "Deploy keys are not allowed to push code.")
else
- raise 'Wrong actor'
+ raise UnauthorizedError, ERROR_MESSAGES[deploy_key ? :deploy_key : :upload]
end
end
+ def guest_can_downlod_code?
+ Guest.can?(:download_code, project)
+ end
+
def user_download_access_check
unless user_can_download_code? || build_can_download_code?
- return build_status_object(false, "You are not allowed to download code from this project.")
+ raise UnauthorizedError, ERROR_MESSAGES[:download]
end
-
- build_status_object(true)
end
def user_can_download_code?
@@ -78,15 +79,15 @@ module Gitlab
def user_push_access_check(changes)
unless authentication_abilities.include?(:push_code)
- return build_status_object(false, "You are not allowed to upload code for this project.")
+ raise UnauthorizedError, ERROR_MESSAGES[:upload]
end
if changes.blank?
- return build_status_object(true)
+ return # Allow access.
end
unless project.repository.exists?
- return build_status_object(false, "A repository for this project does not exist yet.")
+ raise UnauthorizedError, ERROR_MESSAGES[:no_repo]
end
changes_list = Gitlab::ChangesList.new(changes)
@@ -96,11 +97,9 @@ module Gitlab
status = change_access_check(change)
unless status.allowed?
# If user does not have access to make at least one change - cancel all push
- return status
+ raise UnauthorizedError, status.message
end
end
-
- build_status_object(true)
end
def change_access_check(change)
@@ -113,6 +112,30 @@ module Gitlab
private
+ def check_protocol!
+ unless protocol_allowed?
+ raise UnauthorizedError, "Git access over #{protocol.upcase} is not allowed"
+ end
+ end
+
+ def check_active_user!
+ if user && !user_access.allowed?
+ raise UnauthorizedError, "Your account has been blocked."
+ end
+ end
+
+ def check_project_accessibility!
+ if project.blank? || !can_read_project?
+ raise UnauthorizedError, 'The project you were looking for could not be found.'
+ end
+ end
+
+ def check_command_existence!(cmd)
+ unless ALL_COMMANDS.include?(cmd)
+ raise UnauthorizedError, "The command you're trying to execute is not allowed."
+ end
+ end
+
def matching_merge_request?(newrev, branch_name)
Checks::MatchingMergeRequest.new(newrev, branch_name, project).match?
end
@@ -130,6 +153,16 @@ module Gitlab
end
end
+ def can_read_project?
+ if user
+ user_access.can_read_project?
+ elsif deploy_key
+ deploy_key_can_read_project?
+ else
+ Guest.can?(:read_project, project)
+ end
+ end
+
protected
def user
diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb
index f71d3575909..2c06c4ff1ef 100644
--- a/lib/gitlab/git_access_wiki.rb
+++ b/lib/gitlab/git_access_wiki.rb
@@ -1,5 +1,13 @@
module Gitlab
class GitAccessWiki < GitAccess
+ def guest_can_downlod_code?
+ Guest.can?(:download_wiki_code, project)
+ end
+
+ def user_can_download_code?
+ authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_wiki_code)
+ end
+
def change_access_check(change)
if user_access.can_do_action?(:create_wiki)
build_status_object(true)
diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb
index 4750675ae9d..0a8d05b5fe1 100644
--- a/lib/gitlab/github_import/branch_formatter.rb
+++ b/lib/gitlab/github_import/branch_formatter.rb
@@ -8,7 +8,7 @@ module Gitlab
end
def valid?
- repo.present?
+ sha.present? && ref.present?
end
private
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index 90cf38a8513..281b65bdeba 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -20,10 +20,18 @@ module Gitlab
end
def execute
+ # The ordering of importing is important here due to the way GitHub structures their data
+ # 1. Labels are required by other items while not having a dependency on anything else
+ # so need to be first
+ # 2. Pull requests must come before issues. Every pull request is also an issue but not
+ # all issues are pull requests. Only the issue entity has labels defined in GitHub. GitLab
+ # doesn't structure data like this so we need to make sure that we've created the MRs
+ # before we attempt to add the labels defined in the GitHub issue for the related, already
+ # imported, pull request
import_labels
import_milestones
- import_issues
import_pull_requests
+ import_issues
import_comments(:issues)
import_comments(:pull_requests)
import_wiki
@@ -79,13 +87,17 @@ module Gitlab
issues.each do |raw|
gh_issue = IssueFormatter.new(project, raw)
- if gh_issue.valid?
- begin
- issue = gh_issue.create!
- apply_labels(issue, raw)
- rescue => e
- errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
- end
+ begin
+ issuable =
+ if gh_issue.pull_request?
+ MergeRequest.find_by_iid(gh_issue.number)
+ else
+ gh_issue.create!
+ end
+
+ apply_labels(issuable, raw)
+ rescue => e
+ errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
end
end
end
@@ -101,8 +113,7 @@ module Gitlab
restore_source_branch(pull_request) unless pull_request.source_branch_exists?
restore_target_branch(pull_request) unless pull_request.target_branch_exists?
- merge_request = pull_request.create!
- apply_labels(merge_request, raw)
+ pull_request.create!
rescue => e
errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(pull_request.url), errors: e.message }
ensure
@@ -133,21 +144,14 @@ module Gitlab
remove_branch(pull_request.target_branch_name) unless pull_request.target_branch_exists?
end
- def apply_labels(issuable, raw_issuable)
- # GH returns labels for issues but not for pull requests!
- labels = if issuable.is_a?(MergeRequest)
- client.labels_for_issue(repo, raw_issuable.number)
- else
- raw_issuable.labels
- end
+ def apply_labels(issuable, raw)
+ return unless raw.labels.count > 0
- if labels.count > 0
- label_ids = labels
- .map { |attrs| @labels[attrs.name] }
- .compact
+ label_ids = raw.labels
+ .map { |attrs| @labels[attrs.name] }
+ .compact
- issuable.update_attribute(:label_ids, label_ids)
- end
+ issuable.update_attribute(:label_ids, label_ids)
end
def import_comments(issuable_type)
diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb
index 8c32ac59fc5..887690bcc7c 100644
--- a/lib/gitlab/github_import/issue_formatter.rb
+++ b/lib/gitlab/github_import/issue_formatter.rb
@@ -32,8 +32,8 @@ module Gitlab
raw_data.number
end
- def valid?
- raw_data.pull_request.nil?
+ def pull_request?
+ raw_data.pull_request.present?
end
private
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 2c21804fe7a..4d4e04e9e35 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -8,6 +8,8 @@ module Gitlab
gon.shortcuts_path = help_page_path('shortcuts')
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
gon.award_menu_url = emojis_path
+ gon.katex_css_url = ActionController::Base.helpers.asset_path('katex.css')
+ gon.katex_js_url = ActionController::Base.helpers.asset_path('katex.js')
if current_user
gon.current_user_id = current_user.id
diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb
index f8809db21aa..94678b6ec40 100644
--- a/lib/gitlab/identifier.rb
+++ b/lib/gitlab/identifier.rb
@@ -21,10 +21,8 @@ module Gitlab
return if !commit || !commit.author_email
- email = commit.author_email
-
- identify_with_cache(:email, email) do
- User.find_by(email: email)
+ identify_with_cache(:email, commit.author_email) do
+ commit.author
end
end
diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb
index 8b38cfaefb6..7b05290e5cc 100644
--- a/lib/gitlab/ldap/adapter.rb
+++ b/lib/gitlab/ldap/adapter.rb
@@ -89,9 +89,7 @@ module Gitlab
end
def user_filter(filter = nil)
- if config.user_filter.present?
- user_filter = Net::LDAP::Filter.construct(config.user_filter)
- end
+ user_filter = config.constructed_user_filter if config.user_filter.present?
if user_filter && filter
Net::LDAP::Filter.join(filter, user_filter)
diff --git a/lib/gitlab/ldap/authentication.rb b/lib/gitlab/ldap/authentication.rb
index bad683c6511..4745311402c 100644
--- a/lib/gitlab/ldap/authentication.rb
+++ b/lib/gitlab/ldap/authentication.rb
@@ -54,11 +54,9 @@ module Gitlab
# Apply LDAP user filter if present
if config.user_filter.present?
- filter = Net::LDAP::Filter.join(
- filter,
- Net::LDAP::Filter.construct(config.user_filter)
- )
+ filter = Net::LDAP::Filter.join(filter, config.constructed_user_filter)
end
+
filter
end
diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb
index f9bb5775323..de52ef3fc65 100644
--- a/lib/gitlab/ldap/config.rb
+++ b/lib/gitlab/ldap/config.rb
@@ -13,7 +13,7 @@ module Gitlab
end
def self.providers
- servers.map {|server| server['provider_name'] }
+ servers.map { |server| server['provider_name'] }
end
def self.valid_provider?(provider)
@@ -38,13 +38,31 @@ module Gitlab
end
def adapter_options
- {
- host: options['host'],
- port: options['port'],
- encryption: encryption
- }.tap do |options|
- options.merge!(auth_options) if has_auth?
+ opts = base_options.merge(
+ encryption: encryption,
+ )
+
+ opts.merge!(auth_options) if has_auth?
+
+ opts
+ end
+
+ def omniauth_options
+ opts = base_options.merge(
+ base: base,
+ method: options['method'],
+ filter: omniauth_user_filter,
+ name_proc: name_proc
+ )
+
+ if has_auth?
+ opts.merge!(
+ bind_dn: options['bind_dn'],
+ password: options['password']
+ )
end
+
+ opts
end
def base
@@ -68,6 +86,10 @@ module Gitlab
options['user_filter']
end
+ def constructed_user_filter
+ @constructed_user_filter ||= Net::LDAP::Filter.construct(user_filter)
+ end
+
def group_base
options['group_base']
end
@@ -92,8 +114,31 @@ module Gitlab
options['timeout'].to_i
end
+ def has_auth?
+ options['password'] || options['bind_dn']
+ end
+
+ def allow_username_or_email_login
+ options['allow_username_or_email_login']
+ end
+
+ def name_proc
+ if allow_username_or_email_login
+ Proc.new { |name| name.gsub(/@.*\z/, '') }
+ else
+ Proc.new { |name| name }
+ end
+ end
+
protected
+ def base_options
+ {
+ host: options['host'],
+ port: options['port']
+ }
+ end
+
def base_config
Gitlab.config.ldap
end
@@ -123,8 +168,14 @@ module Gitlab
}
end
- def has_auth?
- options['password'] || options['bind_dn']
+ def omniauth_user_filter
+ uid_filter = Net::LDAP::Filter.eq(uid, '%{username}')
+
+ if user_filter.present?
+ Net::LDAP::Filter.join(uid_filter, constructed_user_filter).to_s
+ else
+ uid_filter.to_s
+ end
end
end
end
diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb
index a5220d92312..3503fac40e8 100644
--- a/lib/gitlab/mail_room.rb
+++ b/lib/gitlab/mail_room.rb
@@ -31,6 +31,7 @@ module Gitlab
config[:ssl] = false if config[:ssl].nil?
config[:start_tls] = false if config[:start_tls].nil?
config[:mailbox] = 'inbox' if config[:mailbox].nil?
+ config[:idle_timeout] = 60 if config[:idle_timeout].nil?
if config[:enabled] && config[:address]
gitlab_redis = Gitlab::Redis.new(rails_env)
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
new file mode 100644
index 00000000000..65713e73a59
--- /dev/null
+++ b/lib/gitlab/middleware/multipart.rb
@@ -0,0 +1,99 @@
+# Gitlab::Middleware::Multipart - a Rack::Multipart replacement
+#
+# Rack::Multipart leaves behind tempfiles in /tmp and uses valuable Ruby
+# process time to copy files around. This alternative solution uses
+# gitlab-workhorse to clean up the tempfiles and puts the tempfiles in a
+# location where copying should not be needed.
+#
+# When gitlab-workhorse finds files in a multipart MIME body it sends
+# a signed message via a request header. This message lists the names of
+# the multipart entries that gitlab-workhorse filtered out of the
+# multipart structure and saved to tempfiles. Workhorse adds new entries
+# in the multipart structure with paths to the tempfiles.
+#
+# The job of this Rack middleware is to detect and decode the message
+# from workhorse. If present, it walks the Rack 'params' hash for the
+# current request, opens the respective tempfiles, and inserts the open
+# Ruby File objects in the params hash where Rack::Multipart would have
+# put them. The goal is that application code deeper down can keep
+# working the way it did with Rack::Multipart without changes.
+#
+# CAVEAT: the code that modifies the params hash is a bit complex. It is
+# conceivable that certain Rack params structures will not be modified
+# correctly. We are not aware of such bugs at this time though.
+#
+
+module Gitlab
+ module Middleware
+ class Multipart
+ RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS'
+
+ class Handler
+ def initialize(env, message)
+ @request = Rack::Request.new(env)
+ @rewritten_fields = message['rewritten_fields']
+ @open_files = []
+ end
+
+ def with_open_files
+ @rewritten_fields.each do |field, tmp_path|
+ parsed_field = Rack::Utils.parse_nested_query(field)
+ raise "unexpected field: #{field.inspect}" unless parsed_field.count == 1
+
+ key, value = parsed_field.first
+ if value.nil?
+ value = File.open(tmp_path)
+ @open_files << value
+ else
+ value = decorate_params_value(value, @request.params[key], tmp_path)
+ end
+ @request.update_param(key, value)
+ end
+
+ yield
+ ensure
+ @open_files.each(&:close)
+ end
+
+ # This function calls itself recursively
+ def decorate_params_value(path_hash, value_hash, tmp_path)
+ unless path_hash.is_a?(Hash) && path_hash.count == 1
+ raise "invalid path: #{path_hash.inspect}"
+ end
+ path_key, path_value = path_hash.first
+
+ unless value_hash.is_a?(Hash) && value_hash[path_key]
+ raise "invalid value hash: #{value_hash.inspect}"
+ end
+
+ case path_value
+ when nil
+ value_hash[path_key] = File.open(tmp_path)
+ @open_files << value_hash[path_key]
+ value_hash
+ when Hash
+ decorate_params_value(path_value, value_hash[path_key], tmp_path)
+ value_hash
+ else
+ raise "unexpected path value: #{path_value.inspect}"
+ end
+ end
+ end
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ encoded_message = env.delete(RACK_ENV_KEY)
+ return @app.call(env) if encoded_message.blank?
+
+ message = Gitlab::Workhorse.decode_jwt(encoded_message)[0]
+
+ Handler.new(env, message).with_open_files do
+ @app.call(env)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index 0a91d3918d5..96ed20af918 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -39,7 +39,7 @@ module Gitlab
log.info "(#{provider}) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}"
gl_user
rescue ActiveRecord::RecordInvalid => e
- log.info "(#{provider}) Error saving user: #{gl_user.errors.full_messages}"
+ log.info "(#{provider}) Error saving user #{auth_hash.uid} (#{auth_hash.email}): #{gl_user.errors.full_messages}"
return self, e.record.errors
end
@@ -102,6 +102,8 @@ module Gitlab
Gitlab::LDAP::Config.providers.each do |provider|
adapter = Gitlab::LDAP::Adapter.new(provider)
@ldap_person = Gitlab::LDAP::Person.find_by_uid(auth_hash.uid, adapter)
+ # The `uid` might actually be a DN. Try it next.
+ @ldap_person ||= Gitlab::LDAP::Person.find_by_dn(auth_hash.uid, adapter)
break if @ldap_person
end
@ldap_person
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 24733435a5a..6bdf3db9cb8 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -5,11 +5,7 @@ module Gitlab
def initialize(current_user, project, query, repository_ref = nil)
@current_user = current_user
@project = project
- @repository_ref = if repository_ref.present?
- repository_ref
- else
- nil
- end
+ @repository_ref = repository_ref.presence || project.default_branch
@query = query
end
@@ -44,36 +40,81 @@ module Gitlab
@commits_count ||= commits.count
end
+ def self.parse_search_result(result)
+ ref = nil
+ filename = nil
+ basename = nil
+ startline = 0
+
+ result.each_line.each_with_index do |line, index|
+ if line =~ /^.*:.*:\d+:/
+ ref, filename, startline = line.split(':')
+ startline = startline.to_i - index
+ extname = Regexp.escape(File.extname(filename))
+ basename = filename.sub(/#{extname}$/, '')
+ break
+ end
+ end
+
+ data = ""
+
+ result.each_line do |line|
+ data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '')
+ end
+
+ OpenStruct.new(
+ filename: filename,
+ basename: basename,
+ ref: ref,
+ startline: startline,
+ data: data
+ )
+ end
+
private
def blobs
- if project.empty_repo? || query.blank?
- []
- else
- project.repository.search_files(query, repository_ref)
+ @blobs ||= begin
+ blobs = project.repository.search_files_by_content(query, repository_ref).first(100)
+ found_file_names = Set.new
+
+ results = blobs.map do |blob|
+ blob = self.class.parse_search_result(blob)
+ found_file_names << blob.filename
+
+ [blob.filename, blob]
+ end
+
+ project.repository.search_files_by_name(query, repository_ref).first(100).each do |filename|
+ results << [filename, nil] unless found_file_names.include?(filename)
+ end
+
+ results.sort_by(&:first)
end
end
def wiki_blobs
- if project.wiki_enabled? && query.present?
- project_wiki = ProjectWiki.new(project)
+ @wiki_blobs ||= begin
+ if project.wiki_enabled? && query.present?
+ project_wiki = ProjectWiki.new(project)
- unless project_wiki.empty?
- project_wiki.search_files(query)
+ unless project_wiki.empty?
+ project_wiki.search_files(query)
+ else
+ []
+ end
else
[]
end
- else
- []
end
end
def notes
- project.notes.user.search(query, as_user: @current_user).order('updated_at DESC')
+ @notes ||= NotesFinder.new(project, @current_user, search: query).execute.user.order('updated_at DESC')
end
def commits
- project.repository.find_commits_by_message(query)
+ @commits ||= project.repository.find_commits_by_message(query)
end
def project_ids_relation
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index cb1659f9cee..9e0b0e5ea98 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -2,7 +2,16 @@ module Gitlab
module Regex
extend self
- NAMESPACE_REGEX_STR = '(?:[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_])(?<!\.git|\.atom)'.freeze
+ # The namespace regex is used in Javascript to validate usernames in the "Register" form. However, Javascript
+ # does not support the negative lookbehind assertion (?<!) that disallows usernames ending in `.git` and `.atom`.
+ # Since this is a non-trivial problem to solve in Javascript (heavily complicate the regex, modify view code to
+ # allow non-regex validatiions, etc), `NAMESPACE_REGEX_STR_SIMPLE` serves as a Javascript-compatible version of
+ # `NAMESPACE_REGEX_STR`, with the negative lookbehind assertion removed. This means that the client-side validation
+ # will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation.
+ PATH_REGEX_STR = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*'.freeze
+ NAMESPACE_REGEX_STR_SIMPLE = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze
+ NAMESPACE_REGEX_STR = '(?:' + NAMESPACE_REGEX_STR_SIMPLE + ')(?<!\.git|\.atom)'.freeze
+ PROJECT_REGEX_STR = PATH_REGEX_STR + '(?<!\.git|\.atom)'.freeze
def namespace_regex
@namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze
@@ -26,16 +35,24 @@ module Gitlab
end
def project_name_regex
- @project_name_regex ||= /\A[\p{Alnum}_][\p{Alnum}\p{Pd}_\. ]*\z/.freeze
+ @project_name_regex ||= /\A[\p{Alnum}\u{00A9}-\u{1f9c0}_][\p{Alnum}\p{Pd}\u{00A9}-\u{1f9c0}_\. ]*\z/.freeze
end
def project_name_regex_message
- "can contain only letters, digits, '_', '.', dash and space. " \
- "It must start with letter, digit or '_'."
+ "can contain only letters, digits, emojis, '_', '.', dash, space. " \
+ "It must start with letter, digit, emoji or '_'."
end
def project_path_regex
- @project_path_regex ||= /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\.]*(?<!\.git|\.atom)\z/.freeze
+ @project_path_regex ||= /\A#{PROJECT_REGEX_STR}\z/.freeze
+ end
+
+ def project_route_regex
+ @project_route_regex ||= /#{PROJECT_REGEX_STR}/.freeze
+ end
+
+ def project_git_route_regex
+ @project_route_git_regex ||= /#{PATH_REGEX_STR}\.git/.freeze
end
def project_path_regex_message
@@ -44,7 +61,7 @@ module Gitlab
end
def file_name_regex
- @file_name_regex ||= /\A[a-zA-Z0-9_\-\.\@]*\z/.freeze
+ @file_name_regex ||= /\A[[[:alnum:]]_\-\.\@]*\z/.freeze
end
def file_name_regex_message
@@ -52,7 +69,7 @@ module Gitlab
end
def file_path_regex
- @file_path_regex ||= /\A[a-zA-Z0-9_\-\.\/\@]*\z/.freeze
+ @file_path_regex ||= /\A[[[:alnum:]]_\-\.\/\@]*\z/.freeze
end
def file_path_regex_message
@@ -106,5 +123,22 @@ module Gitlab
def environment_name_regex_message
"can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.' and spaces"
end
+
+ def kubernetes_namespace_regex
+ /\A[a-z0-9]([-a-z0-9]*[a-z0-9])?\z/
+ end
+
+ def kubernetes_namespace_regex_message
+ "can contain only letters, digits or '-', and cannot start or end with '-'"
+ end
+
+ def environment_slug_regex
+ @environment_slug_regex ||= /\A[a-z]([a-z0-9-]*[a-z0-9])?\z/.freeze
+ end
+
+ def environment_slug_regex_message
+ "can contain only lowercase letters, digits, and '-'. " \
+ "Must start with a letter, and cannot end with '-'"
+ end
end
end
diff --git a/lib/gitlab/routing.rb b/lib/gitlab/routing.rb
index 5132177de51..632e2d87500 100644
--- a/lib/gitlab/routing.rb
+++ b/lib/gitlab/routing.rb
@@ -1,5 +1,11 @@
module Gitlab
module Routing
+ extend ActiveSupport::Concern
+
+ included do
+ include Gitlab::Routing.url_helpers
+ end
+
# Returns the URL helpers Module.
#
# This method caches the output as Rails' "url_helpers" method creates an
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 2690938fe82..35212992698 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -50,7 +50,7 @@ module Gitlab
end
def issues
- issues = Issue.visible_to_user(current_user).where(project_id: project_ids_relation)
+ issues = IssuesFinder.new(current_user).execute.where(project_id: project_ids_relation)
if query =~ /#(\d+)\z/
issues = issues.where(iid: $1)
@@ -68,7 +68,7 @@ module Gitlab
end
def merge_requests
- merge_requests = MergeRequest.in_projects(project_ids_relation)
+ merge_requests = MergeRequestsFinder.new(current_user).execute.in_projects(project_ids_relation)
if query =~ /[#!](\d+)\z/
merge_requests = merge_requests.where(iid: $1)
else
diff --git a/lib/gitlab/sidekiq_throttler.rb b/lib/gitlab/sidekiq_throttler.rb
new file mode 100644
index 00000000000..d4d39a888e7
--- /dev/null
+++ b/lib/gitlab/sidekiq_throttler.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ class SidekiqThrottler
+ class << self
+ def execute!
+ if Gitlab::CurrentSettings.sidekiq_throttling_enabled?
+ Gitlab::CurrentSettings.current_application_settings.sidekiq_throttling_queues.each do |queue|
+ Sidekiq::Queue[queue].limit = queue_limit
+ end
+ end
+ end
+
+ private
+
+ def queue_limit
+ @queue_limit ||=
+ begin
+ factor = Gitlab::CurrentSettings.current_application_settings.sidekiq_throttling_factor
+ (factor * Sidekiq.options[:concurrency]).ceil
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb
index 1cd89b3a9c4..222021e8802 100644
--- a/lib/gitlab/sql/union.rb
+++ b/lib/gitlab/sql/union.rb
@@ -22,9 +22,7 @@ module Gitlab
# By using "unprepared_statements" we remove the usage of placeholders
# (thus fixing this problem), at a slight performance cost.
fragments = ActiveRecord::Base.connection.unprepared_statement do
- @relations.map do |rel|
- rel.reorder(nil).to_sql
- end
+ @relations.map { |rel| rel.reorder(nil).to_sql }.reject(&:blank?)
end
fragments.join("\nUNION\n")
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index 99d0c28e749..ccb456bcc94 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -24,6 +24,8 @@ module Gitlab
wiki_page_url
when ProjectSnippet
project_snippet_url(object)
+ when Snippet
+ personal_snippet_url(object)
else
raise NotImplementedError.new("No URL builder defined for #{object.class}")
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 594439a5d4b..aeb1a26e1ba 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -117,8 +117,12 @@ module Gitlab
end
def verify_api_request!(request_headers)
+ decode_jwt(request_headers[INTERNAL_API_REQUEST_HEADER])
+ end
+
+ def decode_jwt(encoded_message)
JWT.decode(
- request_headers[INTERNAL_API_REQUEST_HEADER],
+ encoded_message,
secret,
true,
{ iss: 'gitlab-workhorse', verify_iss: true, algorithm: 'HS256' },
diff --git a/lib/mattermost/presenter.rb b/lib/mattermost/presenter.rb
new file mode 100644
index 00000000000..67eda983a74
--- /dev/null
+++ b/lib/mattermost/presenter.rb
@@ -0,0 +1,131 @@
+module Mattermost
+ class Presenter
+ class << self
+ include Gitlab::Routing.url_helpers
+
+ def authorize_chat_name(url)
+ message = if url
+ ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})."
+ else
+ ":sweat_smile: Couldn't identify you, nor can I autorize you!"
+ end
+
+ ephemeral_response(message)
+ end
+
+ def help(commands, trigger)
+ if commands.none?
+ ephemeral_response("No commands configured")
+ else
+ commands.map! { |command| "#{trigger} #{command}" }
+ message = header_with_list("Available commands", commands)
+
+ ephemeral_response(message)
+ end
+ end
+
+ def present(subject)
+ return not_found unless subject
+
+ if subject.is_a?(Gitlab::ChatCommands::Result)
+ show_result(subject)
+ elsif subject.respond_to?(:count)
+ if subject.many?
+ multiple_resources(subject)
+ elsif subject.none?
+ not_found
+ else
+ single_resource(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)
+ resources.map! { |resource| title(resource) }
+
+ message = header_with_list("Multiple results were found:", resources)
+
+ ephemeral_response(message)
+ end
+
+ def error(resource)
+ message = header_with_list("The action was not successful, because:", resource.errors.messages)
+
+ ephemeral_response(message)
+ end
+
+ def title(resource)
+ 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/mattermost/session.rb b/lib/mattermost/session.rb
new file mode 100644
index 00000000000..fb8d7d97f8a
--- /dev/null
+++ b/lib/mattermost/session.rb
@@ -0,0 +1,115 @@
+module Mattermost
+ class NoSessionError < StandardError; end
+ # This class' prime objective is to obtain a session token on a Mattermost
+ # instance with SSO configured where this GitLab instance is the provider.
+ #
+ # The process depends on OAuth, but skips a step in the authentication cycle.
+ # For example, usually a user would click the 'login in GitLab' button on
+ # Mattermost, which would yield a 302 status code and redirects you to GitLab
+ # to approve the use of your account on Mattermost. Which would trigger a
+ # callback so Mattermost knows this request is approved and gets the required
+ # data to create the user account etc.
+ #
+ # This class however skips the button click, and also the approval phase to
+ # speed up the process and keep it without manual action and get a session
+ # going.
+ class Session
+ include Doorkeeper::Helpers::Controller
+ include HTTParty
+
+ base_uri Settings.mattermost.host
+
+ attr_accessor :current_resource_owner, :token
+
+ def initialize(current_user)
+ @current_resource_owner = current_user
+ end
+
+ def with_session
+ raise NoSessionError unless create
+
+ begin
+ yield self
+ ensure
+ destroy
+ end
+ end
+
+ # Next methods are needed for Doorkeeper
+ def pre_auth
+ @pre_auth ||= Doorkeeper::OAuth::PreAuthorization.new(
+ Doorkeeper.configuration, server.client_via_uid, params)
+ end
+
+ def authorization
+ @authorization ||= strategy.request
+ end
+
+ def strategy
+ @strategy ||= server.authorization_request(pre_auth.response_type)
+ end
+
+ def request
+ @request ||= OpenStruct.new(parameters: params)
+ end
+
+ def params
+ Rack::Utils.parse_query(oauth_uri.query).symbolize_keys
+ end
+
+ def get(path, options = {})
+ self.class.get(path, options.merge(headers: @headers))
+ end
+
+ def post(path, options = {})
+ self.class.post(path, options.merge(headers: @headers))
+ end
+
+ private
+
+ def create
+ return unless oauth_uri
+ return unless token_uri
+
+ @token = request_token
+ @headers = {
+ Authorization: "Bearer #{@token}"
+ }
+
+ @token
+ end
+
+ def destroy
+ post('/api/v3/users/logout')
+ end
+
+ def oauth_uri
+ return @oauth_uri if defined?(@oauth_uri)
+
+ @oauth_uri = nil
+
+ response = get("/api/v3/oauth/gitlab/login", follow_redirects: false)
+ return unless 300 <= response.code && response.code < 400
+
+ redirect_uri = response.headers['location']
+ return unless redirect_uri
+
+ @oauth_uri = URI.parse(redirect_uri)
+ end
+
+ def token_uri
+ @token_uri ||=
+ if oauth_uri
+ authorization.authorize.redirect_uri if pre_auth.authorizable?
+ end
+ end
+
+ def request_token
+ response = get(token_uri, follow_redirects: false)
+
+ if 200 <= response.code && response.code < 400
+ response.headers['token']
+ end
+ end
+ end
+end
diff --git a/lib/omniauth/strategies/bitbucket.rb b/lib/omniauth/strategies/bitbucket.rb
new file mode 100644
index 00000000000..5a7d67c2390
--- /dev/null
+++ b/lib/omniauth/strategies/bitbucket.rb
@@ -0,0 +1,41 @@
+require 'omniauth-oauth2'
+
+module OmniAuth
+ module Strategies
+ class Bitbucket < OmniAuth::Strategies::OAuth2
+ option :name, 'bitbucket'
+
+ option :client_options, {
+ site: 'https://bitbucket.org',
+ authorize_url: 'https://bitbucket.org/site/oauth2/authorize',
+ token_url: 'https://bitbucket.org/site/oauth2/access_token'
+ }
+
+ uid do
+ raw_info['username']
+ end
+
+ info do
+ {
+ name: raw_info['display_name'],
+ avatar: raw_info['links']['avatar']['href'],
+ email: primary_email
+ }
+ end
+
+ def raw_info
+ @raw_info ||= access_token.get('api/2.0/user').parsed
+ end
+
+ def primary_email
+ primary = emails.find { |i| i['is_primary'] && i['is_confirmed'] }
+ primary && primary['email'] || nil
+ end
+
+ def emails
+ email_response = access_token.get('api/2.0/user/emails').parsed
+ @emails ||= email_response && email_response['values'] || nil
+ end
+ end
+ end
+end
diff --git a/lib/rouge/lexers/math.rb b/lib/rouge/lexers/math.rb
new file mode 100644
index 00000000000..80784adfd76
--- /dev/null
+++ b/lib/rouge/lexers/math.rb
@@ -0,0 +1,21 @@
+module Rouge
+ module Lexers
+ class Math < Lexer
+ title "A passthrough lexer used for LaTeX input"
+ desc "A boring lexer that doesn't highlight anything"
+
+ tag 'math'
+ 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/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 2ae48a970ce..35c4194e87c 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -760,7 +760,7 @@ namespace :gitlab do
end
namespace :ldap do
- task :check, [:limit] => :environment do |t, args|
+ task :check, [:limit] => :environment do |_, args|
# Only show up to 100 results because LDAP directories can be very big.
# This setting only affects the `rake gitlab:check` script.
args.with_defaults(limit: 100)
@@ -768,7 +768,7 @@ namespace :gitlab do
start_checking "LDAP"
if Gitlab::LDAP::Config.enabled?
- print_users(args.limit)
+ check_ldap(args.limit)
else
puts 'LDAP is disabled in config/gitlab.yml'
end
@@ -776,21 +776,42 @@ namespace :gitlab do
finished_checking "LDAP"
end
- def print_users(limit)
- puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)"
-
+ def check_ldap(limit)
servers = Gitlab::LDAP::Config.providers
servers.each do |server|
puts "Server: #{server}"
- Gitlab::LDAP::Adapter.open(server) do |adapter|
- users = adapter.users(adapter.config.uid, '*', limit)
- users.each do |user|
- puts "\tDN: #{user.dn}\t #{adapter.config.uid}: #{user.uid}"
+
+ begin
+ Gitlab::LDAP::Adapter.open(server) do |adapter|
+ check_ldap_auth(adapter)
+
+ puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)"
+
+ users = adapter.users(adapter.config.uid, '*', limit)
+ users.each do |user|
+ puts "\tDN: #{user.dn}\t #{adapter.config.uid}: #{user.uid}"
+ end
end
+ rescue Net::LDAP::ConnectionRefusedError, Errno::ECONNREFUSED => e
+ puts "Could not connect to the LDAP server: #{e.message}".color(:red)
end
end
end
+
+ def check_ldap_auth(adapter)
+ auth = adapter.config.has_auth?
+
+ if auth && adapter.ldap.bind
+ message = 'Success'.color(:green)
+ elsif auth
+ message = 'Failed. Check `bind_dn` and `password` configuration values'.color(:red)
+ else
+ message = 'Anonymous. No `bind_dn` or `password` configured'.color(:yellow)
+ end
+
+ puts "LDAP authentication... #{message}"
+ end
end
namespace :repo do
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index b7cbdc6cd78..4a696a52b4d 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -91,5 +91,28 @@ namespace :gitlab do
puts "To block these users run this command with BLOCK=true".color(:yellow)
end
end
+
+ # This is a rake task which removes faulty refs. These refs where only
+ # created in the 8.13.RC cycle, and fixed in the stable builds which were
+ # released. So likely this should only be run once on gitlab.com
+ # Faulty refs are moved so they are kept around, else some features break.
+ desc 'GitLab | Cleanup | Remove faulty deployment refs'
+ task move_faulty_deployment_refs: :environment do
+ projects = Project.where(id: Deployment.select(:project_id).distinct)
+
+ projects.find_each do |project|
+ rugged = project.repository.rugged
+
+ max_iid = project.deployments.maximum(:iid)
+
+ rugged.references.each('refs/environments/**/*') do |ref|
+ id = ref.name.split('/').last.to_i
+ next unless id > max_iid
+
+ project.deployments.find(id).create_ref
+ rugged.references.delete(ref)
+ end
+ end
+ end
end
end
diff --git a/lib/tasks/gitlab/dev.rake b/lib/tasks/gitlab/dev.rake
index 5ee99dfc810..7db0779def8 100644
--- a/lib/tasks/gitlab/dev.rake
+++ b/lib/tasks/gitlab/dev.rake
@@ -1,18 +1,19 @@
namespace :gitlab do
namespace :dev do
desc 'Checks if the branch would apply cleanly to EE'
- task ee_compat_check: :environment do
- return if defined?(Gitlab::License)
- return unless ENV['CI']
+ task :ee_compat_check, [:branch] => :environment do |_, args|
+ opts =
+ if ENV['CI']
+ { branch: ENV['CI_BUILD_REF_NAME'] }
+ else
+ unless args[:branch]
+ puts "Must specify a branch as an argument".color(:red)
+ exit 1
+ end
+ args
+ end
- success =
- Gitlab::EeCompatCheck.new(
- branch: ENV['CI_BUILD_REF_NAME'],
- check_dir: File.expand_path('ee-compat-check', __dir__),
- ce_repo: ENV['CI_BUILD_REPO']
- ).check
-
- if success
+ if Gitlab::EeCompatCheck.new(opts || {}).check
exit 0
else
exit 1
diff --git a/lib/tasks/gitlab/helpers.rake b/lib/tasks/gitlab/helpers.rake
new file mode 100644
index 00000000000..dd2d5861481
--- /dev/null
+++ b/lib/tasks/gitlab/helpers.rake
@@ -0,0 +1,8 @@
+require 'tasks/gitlab/task_helpers'
+
+# Prevent StateMachine warnings from outputting during a cron task
+StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON']
+
+namespace :gitlab do
+ include Gitlab::TaskHelpers
+end
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index 58761a129d4..5a09cd7ce41 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -5,42 +5,23 @@ namespace :gitlab do
warn_user_is_not_gitlab
default_version = Gitlab::Shell.version_required
- default_version_tag = 'v' + default_version
- args.with_defaults(tag: default_version_tag, repo: "https://gitlab.com/gitlab-org/gitlab-shell.git")
+ default_version_tag = "v#{default_version}"
+ args.with_defaults(tag: default_version_tag, repo: 'https://gitlab.com/gitlab-org/gitlab-shell.git')
- user = Gitlab.config.gitlab.user
- home_dir = Rails.env.test? ? Rails.root.join('tmp/tests') : Gitlab.config.gitlab.user_home
gitlab_url = Gitlab.config.gitlab.url
# gitlab-shell requires a / at the end of the url
gitlab_url += '/' unless gitlab_url.end_with?('/')
target_dir = Gitlab.config.gitlab_shell.path
- # Clone if needed
- if File.directory?(target_dir)
- Dir.chdir(target_dir) do
- system(*%W(Gitlab.config.git.bin_path} fetch --tags --quiet))
- system(*%W(Gitlab.config.git.bin_path} checkout --quiet #{default_version_tag}))
- end
- else
- system(*%W(#{Gitlab.config.git.bin_path} clone -- #{args.repo} #{target_dir}))
- end
+ checkout_or_clone_tag(tag: default_version_tag, repo: args.repo, target_dir: target_dir)
# Make sure we're on the right tag
Dir.chdir(target_dir) do
- # First try to checkout without fetching
- # to avoid stalling tests if the Internet is down.
- reseted = reset_to_commit(args)
-
- unless reseted
- system(*%W(#{Gitlab.config.git.bin_path} fetch origin))
- reset_to_commit(args)
- end
-
config = {
- user: user,
+ user: Gitlab.config.gitlab.user,
gitlab_url: gitlab_url,
http_settings: {self_signed_cert: false}.stringify_keys,
- auth_file: File.join(home_dir, ".ssh", "authorized_keys"),
+ auth_file: File.join(user_home, ".ssh", "authorized_keys"),
redis: {
bin: %x{which redis-cli}.chomp,
namespace: "resque:gitlab"
@@ -74,7 +55,7 @@ namespace :gitlab do
# be an issue since it is more than likely that there are no "normal"
# user accounts on a gitlab server). The alternative is for the admin to
# install a ruby (1.9.3+) in the global path.
- File.open(File.join(home_dir, ".ssh", "environment"), "w+") do |f|
+ File.open(File.join(user_home, ".ssh", "environment"), "w+") do |f|
f.puts "PATH=#{ENV['PATH']}"
end
@@ -142,15 +123,4 @@ namespace :gitlab do
puts "Quitting...".color(:red)
exit 1
end
-
- def reset_to_commit(args)
- tag, status = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} describe -- #{args.tag}))
-
- unless status.zero?
- tag, status = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} describe -- origin/#{args.tag}))
- end
-
- tag = tag.strip
- system(*%W(#{Gitlab.config.git.bin_path} reset --hard #{tag}))
- end
end
diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake
deleted file mode 100644
index 74be413423a..00000000000
--- a/lib/tasks/gitlab/task_helpers.rake
+++ /dev/null
@@ -1,140 +0,0 @@
-module Gitlab
- class TaskAbortedByUserError < StandardError; end
-end
-
-require 'rainbow/ext/string'
-
-# Prevent StateMachine warnings from outputting during a cron task
-StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON']
-
-namespace :gitlab do
-
- # Ask if the user wants to continue
- #
- # Returns "yes" the user chose to continue
- # Raises Gitlab::TaskAbortedByUserError if the user chose *not* to continue
- def ask_to_continue
- answer = prompt("Do you want to continue (yes/no)? ".color(:blue), %w{yes no})
- raise Gitlab::TaskAbortedByUserError unless answer == "yes"
- end
-
- # Check which OS is running
- #
- # It will primarily use lsb_relase to determine the OS.
- # It has fallbacks to Debian, SuSE, OS X and systems running systemd.
- def os_name
- os_name = run_command(%W(lsb_release -irs))
- os_name ||= if File.readable?('/etc/system-release')
- File.read('/etc/system-release')
- end
- os_name ||= if File.readable?('/etc/debian_version')
- debian_version = File.read('/etc/debian_version')
- "Debian #{debian_version}"
- end
- os_name ||= if File.readable?('/etc/SuSE-release')
- File.read('/etc/SuSE-release')
- end
- os_name ||= if os_x_version = run_command(%W(sw_vers -productVersion))
- "Mac OS X #{os_x_version}"
- end
- os_name ||= if File.readable?('/etc/os-release')
- File.read('/etc/os-release').match(/PRETTY_NAME=\"(.+)\"/)[1]
- end
- os_name.try(:squish!)
- end
-
- # Prompt the user to input something
- #
- # message - the message to display before input
- # choices - array of strings of acceptable answers or nil for any answer
- #
- # Returns the user's answer
- def prompt(message, choices = nil)
- begin
- print(message)
- answer = STDIN.gets.chomp
- end while choices.present? && !choices.include?(answer)
- answer
- end
-
- # Runs the given command and matches the output against the given pattern
- #
- # Returns nil if nothing matched
- # Returns the MatchData if the pattern matched
- #
- # see also #run_command
- # see also String#match
- def run_and_match(command, regexp)
- run_command(command).try(:match, regexp)
- end
-
- # Runs the given command
- #
- # Returns nil if the command was not found
- # Returns the output of the command otherwise
- #
- # see also #run_and_match
- def run_command(command)
- output, _ = Gitlab::Popen.popen(command)
- output
- rescue Errno::ENOENT
- '' # if the command does not exist, return an empty string
- end
-
- def uid_for(user_name)
- run_command(%W(id -u #{user_name})).chomp.to_i
- end
-
- def gid_for(group_name)
- begin
- Etc.getgrnam(group_name).gid
- rescue ArgumentError # no group
- "group #{group_name} doesn't exist"
- end
- end
-
- def warn_user_is_not_gitlab
- unless @warned_user_not_gitlab
- gitlab_user = Gitlab.config.gitlab.user
- current_user = run_command(%W(whoami)).chomp
- unless current_user == gitlab_user
- puts " Warning ".color(:black).background(:yellow)
- puts " You are running as user #{current_user.color(:magenta)}, we hope you know what you are doing."
- puts " Things may work\/fail for the wrong reasons."
- puts " For correct results you should run this as user #{gitlab_user.color(:magenta)}."
- puts ""
- end
- @warned_user_not_gitlab = true
- end
- end
-
- # Tries to configure git itself
- #
- # Returns true if all subcommands were successfull (according to their exit code)
- # Returns false if any or all subcommands failed.
- def auto_fix_git_config(options)
- if !@warned_user_not_gitlab
- command_success = options.map do |name, value|
- system(*%W(#{Gitlab.config.git.bin_path} config --global #{name} #{value}))
- end
-
- command_success.all?
- else
- false
- end
- end
-
- def all_repos
- Gitlab.config.repositories.storages.each do |name, path|
- IO.popen(%W(find #{path} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find|
- find.each_line do |path|
- yield path.chomp
- end
- end
- end
- end
-
- def repository_storage_paths_args
- Gitlab.config.repositories.storages.values
- end
-end
diff --git a/lib/tasks/gitlab/task_helpers.rb b/lib/tasks/gitlab/task_helpers.rb
new file mode 100644
index 00000000000..e128738b5f8
--- /dev/null
+++ b/lib/tasks/gitlab/task_helpers.rb
@@ -0,0 +1,190 @@
+require 'rainbow/ext/string'
+
+module Gitlab
+ TaskFailedError = Class.new(StandardError)
+ TaskAbortedByUserError = Class.new(StandardError)
+
+ module TaskHelpers
+ # Ask if the user wants to continue
+ #
+ # Returns "yes" the user chose to continue
+ # Raises Gitlab::TaskAbortedByUserError if the user chose *not* to continue
+ def ask_to_continue
+ answer = prompt("Do you want to continue (yes/no)? ".color(:blue), %w{yes no})
+ raise Gitlab::TaskAbortedByUserError unless answer == "yes"
+ end
+
+ # Check which OS is running
+ #
+ # It will primarily use lsb_relase to determine the OS.
+ # It has fallbacks to Debian, SuSE, OS X and systems running systemd.
+ def os_name
+ os_name = run_command(%W(lsb_release -irs))
+ os_name ||= if File.readable?('/etc/system-release')
+ File.read('/etc/system-release')
+ end
+ os_name ||= if File.readable?('/etc/debian_version')
+ debian_version = File.read('/etc/debian_version')
+ "Debian #{debian_version}"
+ end
+ os_name ||= if File.readable?('/etc/SuSE-release')
+ File.read('/etc/SuSE-release')
+ end
+ os_name ||= if os_x_version = run_command(%W(sw_vers -productVersion))
+ "Mac OS X #{os_x_version}"
+ end
+ os_name ||= if File.readable?('/etc/os-release')
+ File.read('/etc/os-release').match(/PRETTY_NAME=\"(.+)\"/)[1]
+ end
+ os_name.try(:squish!)
+ end
+
+ # Prompt the user to input something
+ #
+ # message - the message to display before input
+ # choices - array of strings of acceptable answers or nil for any answer
+ #
+ # Returns the user's answer
+ def prompt(message, choices = nil)
+ begin
+ print(message)
+ answer = STDIN.gets.chomp
+ end while choices.present? && !choices.include?(answer)
+ answer
+ end
+
+ # Runs the given command and matches the output against the given pattern
+ #
+ # Returns nil if nothing matched
+ # Returns the MatchData if the pattern matched
+ #
+ # see also #run_command
+ # see also String#match
+ def run_and_match(command, regexp)
+ run_command(command).try(:match, regexp)
+ end
+
+ # Runs the given command
+ #
+ # Returns '' if the command was not found
+ # Returns the output of the command otherwise
+ #
+ # see also #run_and_match
+ def run_command(command)
+ output, _ = Gitlab::Popen.popen(command)
+ output
+ rescue Errno::ENOENT
+ '' # if the command does not exist, return an empty string
+ end
+
+ # Runs the given command and raises a Gitlab::TaskFailedError exception if
+ # the command does not exit with 0
+ #
+ # Returns the output of the command otherwise
+ def run_command!(command)
+ output, status = Gitlab::Popen.popen(command)
+
+ raise Gitlab::TaskFailedError unless status.zero?
+
+ output
+ end
+
+ def uid_for(user_name)
+ run_command(%W(id -u #{user_name})).chomp.to_i
+ end
+
+ def gid_for(group_name)
+ begin
+ Etc.getgrnam(group_name).gid
+ rescue ArgumentError # no group
+ "group #{group_name} doesn't exist"
+ end
+ end
+
+ def warn_user_is_not_gitlab
+ unless @warned_user_not_gitlab
+ gitlab_user = Gitlab.config.gitlab.user
+ current_user = run_command(%W(whoami)).chomp
+ unless current_user == gitlab_user
+ puts " Warning ".color(:black).background(:yellow)
+ puts " You are running as user #{current_user.color(:magenta)}, we hope you know what you are doing."
+ puts " Things may work\/fail for the wrong reasons."
+ puts " For correct results you should run this as user #{gitlab_user.color(:magenta)}."
+ puts ""
+ end
+ @warned_user_not_gitlab = true
+ end
+ end
+
+ # Tries to configure git itself
+ #
+ # Returns true if all subcommands were successfull (according to their exit code)
+ # Returns false if any or all subcommands failed.
+ def auto_fix_git_config(options)
+ if !@warned_user_not_gitlab
+ command_success = options.map do |name, value|
+ system(*%W(#{Gitlab.config.git.bin_path} config --global #{name} #{value}))
+ end
+
+ command_success.all?
+ else
+ false
+ end
+ end
+
+ def all_repos
+ Gitlab.config.repositories.storages.each do |name, path|
+ IO.popen(%W(find #{path} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find|
+ find.each_line do |path|
+ yield path.chomp
+ end
+ end
+ end
+ end
+
+ def repository_storage_paths_args
+ Gitlab.config.repositories.storages.values
+ end
+
+ def user_home
+ Rails.env.test? ? Rails.root.join('tmp/tests') : Gitlab.config.gitlab.user_home
+ end
+
+ def checkout_or_clone_tag(tag:, repo:, target_dir:)
+ if Dir.exist?(target_dir)
+ checkout_tag(tag, target_dir)
+ else
+ clone_repo(repo, target_dir)
+ end
+
+ reset_to_tag(tag, target_dir)
+ end
+
+ def clone_repo(repo, target_dir)
+ run_command!(%W[#{Gitlab.config.git.bin_path} clone -- #{repo} #{target_dir}])
+ end
+
+ def checkout_tag(tag, target_dir)
+ run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch --tags --quiet])
+ run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} checkout --quiet #{tag}])
+ end
+
+ def reset_to_tag(tag_wanted, target_dir)
+ tag =
+ begin
+ # First try to checkout without fetching
+ # to avoid stalling tests if the Internet is down.
+ run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- #{tag_wanted}])
+ rescue Gitlab::TaskFailedError
+ run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch origin])
+ run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- origin/#{tag_wanted}])
+ end
+
+ if tag
+ run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} reset --hard #{tag.strip}])
+ else
+ raise Gitlab::TaskFailedError
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/workhorse.rake b/lib/tasks/gitlab/workhorse.rake
new file mode 100644
index 00000000000..baea94bf8ca
--- /dev/null
+++ b/lib/tasks/gitlab/workhorse.rake
@@ -0,0 +1,23 @@
+namespace :gitlab do
+ namespace :workhorse do
+ desc "GitLab | Install or upgrade gitlab-workhorse"
+ task :install, [:dir] => :environment do |t, args|
+ warn_user_is_not_gitlab
+ unless args.dir.present?
+ abort %(Please specify the directory where you want to install gitlab-workhorse:\n rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]")
+ end
+
+ tag = "v#{Gitlab::Workhorse.version}"
+ repo = 'https://gitlab.com/gitlab-org/gitlab-workhorse.git'
+
+ checkout_or_clone_tag(tag: tag, repo: repo, target_dir: args.dir)
+
+ _, status = Gitlab::Popen.popen(%w[which gmake])
+ command = status.zero? ? 'gmake' : 'make'
+
+ Dir.chdir(args.dir) do
+ run_command!([command])
+ end
+ end
+ end
+end
diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake
index 141a0b74ec0..f5caca3ddbf 100644
--- a/lib/tasks/migrate/setup_postgresql.rake
+++ b/lib/tasks/migrate/setup_postgresql.rake
@@ -1,8 +1,12 @@
+require Rails.root.join('lib/gitlab/database')
+require Rails.root.join('lib/gitlab/database/migration_helpers')
require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lower_indexes')
require Rails.root.join('db/migrate/20151008110232_add_users_lower_username_email_indexes')
+require Rails.root.join('db/migrate/20161212142807_add_lower_path_index_to_routes')
desc 'GitLab | Sets up PostgreSQL'
task setup_postgresql: :environment do
NamespacesProjectsPathLowerIndexes.new.up
AddUsersLowerUsernameEmailIndexes.new.up
+ AddLowerPathIndexToRoutes.new.up
end
diff --git a/package.json b/package.json
index a303c9c1eac..49b8210e427 100644
--- a/package.json
+++ b/package.json
@@ -1,15 +1,16 @@
{
"private": true,
"scripts": {
- "eslint": "eslint --ext .js,.js.es6 .",
- "eslint-fix": "eslint --fix --ext .js,.js.es6 ."
+ "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"
},
"devDependencies": {
- "eslint": "^3.1.1",
- "eslint-config-airbnb": "^12.0.0",
+ "eslint": "^3.10.1",
+ "eslint-config-airbnb-base": "^10.0.1",
"eslint-plugin-filenames": "^1.1.0",
- "eslint-plugin-import": "^2.0.1",
- "eslint-plugin-jsx-a11y": "^2.2.3",
- "eslint-plugin-react": "^6.4.1"
+ "eslint-plugin-import": "^2.2.0",
+ "eslint-plugin-jasmine": "^2.1.0",
+ "istanbul": "^0.4.5"
}
}
diff --git a/public/404.html b/public/404.html
index 92b7f4da0b9..b3b3a0fa3f3 100644
--- a/public/404.html
+++ b/public/404.html
@@ -42,6 +42,16 @@
img {
max-width: 40vw;
+ display: block;
+ margin: 40px auto;
+ }
+
+ a {
+ line-height: 100px;
+ font-weight: normal;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
}
.container {
@@ -51,8 +61,9 @@
</head>
<body>
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo="
+ alt="GitLab Logo" />
<h1>
- <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br />
404
</h1>
<div class="container">
@@ -60,6 +71,7 @@
<hr />
<p>Make sure the address is correct and that the page hasn't moved.</p>
<p>Please contact your GitLab administrator if you think this is a mistake.</p>
+ <a href="javascript:history.back()">Go back</a>
</div>
</body>
</html>
diff --git a/public/422.html b/public/422.html
index f625f8a33b7..119e54ad8bd 100644
--- a/public/422.html
+++ b/public/422.html
@@ -42,6 +42,16 @@
img {
max-width: 40vw;
+ display: block;
+ margin: 40px auto;
+ }
+
+ a {
+ line-height: 100px;
+ font-weight: normal;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
}
.container {
@@ -51,8 +61,9 @@
</head>
<body>
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo="
+ alt="GitLab Logo" />
<h1>
- <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br />
422
</h1>
<div class="container">
@@ -60,6 +71,7 @@
<hr />
<p>Make sure you have access to the thing you tried to change.</p>
<p>Please contact your GitLab administrator if you think this is a mistake.</p>
+ <a href="javascript:history.back()">Go back</a>
</div>
</body>
</html>
diff --git a/public/500.html b/public/500.html
index d76c66ba92a..226ef3c40ea 100644
--- a/public/500.html
+++ b/public/500.html
@@ -42,6 +42,16 @@
img {
max-width: 40vw;
+ display: block;
+ margin: 40px auto;
+ }
+
+ a {
+ line-height: 100px;
+ font-weight: normal;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
}
.container {
@@ -51,8 +61,9 @@
</head>
<body>
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo="
+ alt="GitLab Logo" />
<h1>
- <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br />
500
</h1>
<div class="container">
@@ -60,6 +71,7 @@
<hr />
<p>Try refreshing the page, or going back and attempting the action again.</p>
<p>Please contact your GitLab administrator if this problem persists.</p>
+ <a href="javascript:history.back()">Go back</a>
</div>
</body>
</html>
diff --git a/public/502.html b/public/502.html
index 1a3c7efc769..f037b81bace 100644
--- a/public/502.html
+++ b/public/502.html
@@ -42,6 +42,16 @@
img {
max-width: 40vw;
+ display: block;
+ margin: 40px auto;
+ }
+
+ a {
+ line-height: 100px;
+ font-weight: normal;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
}
.container {
@@ -51,8 +61,9 @@
</head>
<body>
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo="
+ alt="GitLab Logo" />
<h1>
- <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br />
502
</h1>
<div class="container">
@@ -60,6 +71,7 @@
<hr />
<p>Try refreshing the page, or going back and attempting the action again.</p>
<p>Please contact your GitLab administrator if this problem persists.</p>
+ <a href="javascript:history.back()">Go back</a>
</div>
</body>
</html>
diff --git a/public/503.html b/public/503.html
index c1c4e3ffdb8..f946a087871 100644
--- a/public/503.html
+++ b/public/503.html
@@ -42,6 +42,16 @@
img {
max-width: 40vw;
+ display: block;
+ margin: 40px auto;
+ }
+
+ a {
+ line-height: 100px;
+ font-weight: normal;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
}
.container {
@@ -51,8 +61,9 @@
</head>
<body>
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo="
+ alt="GitLab Logo" />
<h1>
- <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br />
503
</h1>
<div class="container">
@@ -60,6 +71,7 @@
<hr />
<p>Try refreshing the page, or going back and attempting the action again.</p>
<p>Please contact your GitLab administrator if this problem persists.</p>
+ <a href="javascript:history.back()">Go back</a>
</div>
</body>
</html>
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
index 7c4e8276902..62236ed539a 100755
--- a/scripts/lint-doc.sh
+++ b/scripts/lint-doc.sh
@@ -3,7 +3,7 @@
cd "$(dirname "$0")/.."
# Use long options (e.g. --header instead of -H) for curl examples in documentation.
-grep --perl-regexp --recursive --color=auto 'curl (.+ )?-[^- ].*' doc/
+grep --extended-regexp --recursive --color=auto 'curl (.+ )?-[^- ].*' doc/
if [ $? == 0 ]
then
echo '✖ ERROR: Short options should not be used in documentation!' >&2
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 1eaafdce389..6e3f76b8399 100755
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/bin/sh
retry() {
if eval "$@"; then
@@ -24,11 +24,12 @@ if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then
cp config/resque.yml.example config/resque.yml
sed -i 's/localhost/redis/g' config/resque.yml
- export FLAGS=(--path vendor --retry 3 --quiet)
+ export FLAGS="--path vendor --retry 3 --quiet"
else
- export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin
+ rnd=$(awk 'BEGIN { srand() ; printf("%d\n",rand()*5) }')
+ export PATH="$HOME/bin:/usr/local/bin:/usr/bin:/bin"
cp config/database.yml.mysql config/database.yml
sed "s/username\:.*$/username\: runner/" -i config/database.yml
sed "s/password\:.*$/password\: 'password'/" -i config/database.yml
- sed "s/gitlabhq_test/gitlabhq_test_$((RANDOM/5000))/" -i config/database.yml
+ sed "s/gitlabhq_test/gitlabhq_test_$rnd/" -i config/database.yml
fi
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb
index 22bf3055538..294fae95752 100644
--- a/spec/config/mail_room_spec.rb
+++ b/spec/config/mail_room_spec.rb
@@ -47,6 +47,7 @@ describe 'mail_room.yml' do
expect(mailbox[:email]).to eq('gitlab-incoming@gmail.com')
expect(mailbox[:password]).to eq('[REDACTED]')
expect(mailbox[:name]).to eq('inbox')
+ expect(mailbox[:idle_timeout]).to eq(60)
redis_url = gitlab_redis.url
sentinels = gitlab_redis.sentinels
diff --git a/spec/controllers/admin/impersonations_controller_spec.rb b/spec/controllers/admin/impersonations_controller_spec.rb
index 8be662974a0..8f1f0ba89ff 100644
--- a/spec/controllers/admin/impersonations_controller_spec.rb
+++ b/spec/controllers/admin/impersonations_controller_spec.rb
@@ -76,18 +76,32 @@ describe Admin::ImpersonationsController do
end
context "when the impersonator is not blocked" do
- it "redirects to the impersonated user's page" do
- expect(Gitlab::AppLogger).to receive(:info).with("User #{impersonator.username} has stopped impersonating #{user.username}").and_call_original
+ shared_examples_for "successfully stops impersonating" do
+ it "redirects to the impersonated user's page" do
+ expect(Gitlab::AppLogger).to receive(:info).with("User #{impersonator.username} has stopped impersonating #{user.username}").and_call_original
- delete :destroy
+ delete :destroy
+
+ expect(response).to redirect_to(admin_user_path(user))
+ end
+
+ it "signs us in as the impersonator" do
+ delete :destroy
- expect(response).to redirect_to(admin_user_path(user))
+ expect(warden.user).to eq(impersonator)
+ end
end
- it "signs us in as the impersonator" do
- delete :destroy
+ # base case
+ it_behaves_like "successfully stops impersonating"
+
+ context "and the user has a temporary oauth e-mail address" do
+ before do
+ allow(user).to receive(:temp_oauth_email?).and_return(true)
+ allow(controller).to receive(:current_user).and_return(user)
+ end
- expect(warden.user).to eq(impersonator)
+ it_behaves_like "successfully stops impersonating"
end
end
end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 98e912f000c..81cbccd5436 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
describe ApplicationController do
+ let(:user) { create(:user) }
+
describe '#check_password_expiration' do
- let(:user) { create(:user) }
let(:controller) { ApplicationController.new }
it 'redirects if the user is over their password expiry' do
@@ -39,8 +40,6 @@ describe ApplicationController do
end
end
- let(:user) { create(:user) }
-
context "when the 'private_token' param is populated with the private token" do
it "logs the user in" do
get :index, private_token: user.private_token
@@ -73,7 +72,6 @@ describe ApplicationController do
end
end
- let(:user) { create(:user) }
let(:personal_access_token) { create(:personal_access_token, user: user) }
context "when the 'personal_access_token' param is populated with the personal access token" do
@@ -100,4 +98,21 @@ describe ApplicationController do
end
end
end
+
+ describe '#route_not_found' do
+ let(:controller) { ApplicationController.new }
+
+ it 'renders 404 if authenticated' do
+ allow(controller).to receive(:current_user).and_return(user)
+ expect(controller).to receive(:not_found)
+ controller.send(:route_not_found)
+ end
+
+ it 'does redirect to login page if not authenticated' do
+ allow(controller).to receive(:current_user).and_return(nil)
+ expect(controller).to receive(:redirect_to)
+ expect(controller).to receive(:new_user_session_path)
+ controller.send(:route_not_found)
+ end
+ end
end
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index a121cb2fc97..ea2fd90a9b0 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -4,14 +4,14 @@ describe AutocompleteController do
let!(:project) { create(:project) }
let!(:user) { create(:user) }
- context 'users and members' do
+ context 'GET users' do
let!(:user2) { create(:user) }
let!(:non_member) { create(:user) }
context 'project members' do
before do
sign_in(user)
- project.team << [user, :master]
+ project.add_master(user)
end
describe 'GET #users with project ID' do
@@ -69,7 +69,7 @@ describe AutocompleteController do
before do
sign_in(non_member)
- project.team << [user, :master]
+ project.add_master(user)
end
let(:body) { JSON.parse(response.body) }
@@ -103,7 +103,7 @@ describe AutocompleteController do
describe 'GET #users with public project' do
before do
- public_project.team << [user, :guest]
+ public_project.add_guest(user)
get(:users, project_id: public_project.id)
end
@@ -129,7 +129,7 @@ describe AutocompleteController do
describe 'GET #users with inaccessible group' do
before do
- project.team << [user, :guest]
+ project.add_guest(user)
get(:users, group_id: user.namespace.id)
end
@@ -144,6 +144,15 @@ describe AutocompleteController do
it { expect(body).to be_kind_of(Array) }
it { expect(body.size).to eq 0 }
end
+
+ describe 'GET #users with todo filter' do
+ it 'gives an array of users' do
+ get :users, todo_filter: true
+
+ expect(response.status).to eq 200
+ expect(body).to be_kind_of(Array)
+ end
+ end
end
context 'author of issuable included' do
@@ -180,18 +189,18 @@ describe AutocompleteController do
end
end
- context 'projects' do
+ context 'GET projects' do
let(:authorized_project) { create(:project) }
let(:authorized_search_project) { create(:project, name: 'rugged') }
before do
sign_in(user)
- project.team << [user, :master]
+ project.add_master(user)
end
context 'authorized projects' do
before do
- authorized_project.team << [user, :master]
+ authorized_project.add_master(user)
end
describe 'GET #projects with project ID' do
@@ -216,8 +225,8 @@ describe AutocompleteController do
context 'authorized projects and search' do
before do
- authorized_project.team << [user, :master]
- authorized_search_project.team << [user, :master]
+ authorized_project.add_master(user)
+ authorized_search_project.add_master(user)
end
describe 'GET #projects with project ID and search' do
@@ -242,9 +251,9 @@ describe AutocompleteController do
authorized_project2 = create(:project)
authorized_project3 = create(:project)
- authorized_project.team << [user, :master]
- authorized_project2.team << [user, :master]
- authorized_project3.team << [user, :master]
+ authorized_project.add_master(user)
+ authorized_project2.add_master(user)
+ authorized_project3.add_master(user)
stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
end
@@ -268,9 +277,9 @@ describe AutocompleteController do
authorized_project2 = create(:project)
authorized_project3 = create(:project)
- authorized_project.team << [user, :master]
- authorized_project2.team << [user, :master]
- authorized_project3.team << [user, :master]
+ authorized_project.add_master(user)
+ authorized_project2.add_master(user)
+ authorized_project3.add_master(user)
end
describe 'GET #projects with project ID and offset_id' do
@@ -289,7 +298,7 @@ describe AutocompleteController do
context 'authorized projects without admin_issue ability' do
before(:each) do
- authorized_project.team << [user, :guest]
+ authorized_project.add_guest(user)
expect(user.can?(:admin_issue, authorized_project)).to eq(false)
end
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index c7db84dd5f9..60db0192dfd 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Groups::GroupMembersController do
let(:user) { create(:user) }
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :access_requestable) }
describe 'GET index' do
it 'renders index with 200 status code' do
diff --git a/spec/controllers/groups/labels_controller_spec.rb b/spec/controllers/groups/labels_controller_spec.rb
new file mode 100644
index 00000000000..899d8ebd12b
--- /dev/null
+++ b/spec/controllers/groups/labels_controller_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Groups::LabelsController do
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+
+ before do
+ group.add_owner(user)
+
+ sign_in(user)
+ end
+
+ describe 'POST #toggle_subscription' do
+ it 'allows user to toggle subscription on group labels' do
+ label = create(:group_label, group: group)
+
+ post :toggle_subscription, group_id: group.to_param, id: label.to_param
+
+ expect(response).to have_http_status(200)
+ end
+ end
+end
diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb
index 33c75e7584f..d3489324a9c 100644
--- a/spec/controllers/help_controller_spec.rb
+++ b/spec/controllers/help_controller_spec.rb
@@ -7,6 +7,38 @@ describe HelpController do
sign_in(user)
end
+ describe 'GET #index' do
+ context 'with absolute url' do
+ it 'keeps the URL absolute' do
+ stub_readme("[API](/api/README.md)")
+
+ get :index
+
+ expect(assigns[:help_index]).to eq '[API](/api/README.md)'
+ end
+ end
+
+ context 'with relative url' do
+ it 'prefixes it with /help/' do
+ stub_readme("[API](api/README.md)")
+
+ get :index
+
+ expect(assigns[:help_index]).to eq '[API](/help/api/README.md)'
+ end
+ end
+
+ context 'when url is an external link' do
+ it 'does not change it' do
+ stub_readme("[external](https://some.external.link)")
+
+ get :index
+
+ expect(assigns[:help_index]).to eq '[external](https://some.external.link)'
+ end
+ end
+ end
+
describe 'GET #show' do
context 'for Markdown formats' do
context 'when requested file exists' do
@@ -72,4 +104,8 @@ describe HelpController do
end
end
end
+
+ def stub_readme(content)
+ allow(File).to receive(:read).and_return(content)
+ end
end
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 1d3c9fbbe2f..ce7c0b334ee 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -6,11 +6,11 @@ describe Import::BitbucketController do
let(:user) { create(:user) }
let(:token) { "asdasd12345" }
let(:secret) { "sekrettt" }
- let(:access_params) { { bitbucket_access_token: token, bitbucket_access_token_secret: secret } }
+ let(:refresh_token) { SecureRandom.hex(15) }
+ let(:access_params) { { token: token, expires_at: nil, expires_in: nil, refresh_token: nil } }
def assign_session_tokens
- session[:bitbucket_access_token] = token
- session[:bitbucket_access_token_secret] = secret
+ session[:bitbucket_token] = token
end
before do
@@ -24,29 +24,36 @@ describe Import::BitbucketController do
end
it "updates access token" do
- access_token = double(token: token, secret: secret)
- allow_any_instance_of(Gitlab::BitbucketImport::Client).
+ expires_at = Time.now + 1.day
+ expires_in = 1.day
+ access_token = double(token: token,
+ secret: secret,
+ expires_at: expires_at,
+ expires_in: expires_in,
+ refresh_token: refresh_token)
+ allow_any_instance_of(OAuth2::Client).
to receive(:get_token).and_return(access_token)
stub_omniauth_provider('bitbucket')
get :callback
- expect(session[:bitbucket_access_token]).to eq(token)
- expect(session[:bitbucket_access_token_secret]).to eq(secret)
+ expect(session[:bitbucket_token]).to eq(token)
+ expect(session[:bitbucket_refresh_token]).to eq(refresh_token)
+ expect(session[:bitbucket_expires_at]).to eq(expires_at)
+ expect(session[:bitbucket_expires_in]).to eq(expires_in)
expect(controller).to redirect_to(status_import_bitbucket_url)
end
end
describe "GET status" do
before do
- @repo = OpenStruct.new(slug: 'vim', owner: 'asd')
+ @repo = double(slug: 'vim', owner: 'asd', full_name: 'asd/vim', "valid?" => true)
assign_session_tokens
end
it "assigns variables" do
@project = create(:project, import_type: 'bitbucket', creator_id: user.id)
- client = stub_client(projects: [@repo])
- allow(client).to receive(:incompatible_projects).and_return([])
+ allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo])
get :status
@@ -57,7 +64,7 @@ describe Import::BitbucketController do
it "does not show already added project" do
@project = create(:project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim')
- stub_client(projects: [@repo])
+ allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo])
get :status
@@ -70,19 +77,16 @@ describe Import::BitbucketController do
let(:bitbucket_username) { user.username }
let(:bitbucket_user) do
- { user: { username: bitbucket_username } }.with_indifferent_access
+ double(username: bitbucket_username)
end
let(:bitbucket_repo) do
- { slug: "vim", owner: bitbucket_username }.with_indifferent_access
+ double(slug: "vim", owner: bitbucket_username, name: 'vim')
end
before do
- allow(Gitlab::BitbucketImport::KeyAdder).
- to receive(:new).with(bitbucket_repo, user, access_params).
- and_return(double(execute: true))
-
- stub_client(user: bitbucket_user, project: bitbucket_repo)
+ allow_any_instance_of(Bitbucket::Client).to receive(:repo).and_return(bitbucket_repo)
+ allow_any_instance_of(Bitbucket::Client).to receive(:user).and_return(bitbucket_user)
assign_session_tokens
end
@@ -90,7 +94,7 @@ describe Import::BitbucketController do
context "when the Bitbucket user and GitLab user's usernames match" do
it "takes the current user's namespace" do
expect(Gitlab::BitbucketImport::ProjectCreator).
- to receive(:new).with(bitbucket_repo, user.namespace, user, access_params).
+ to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params).
and_return(double(execute: true))
post :create, format: :js
@@ -102,7 +106,7 @@ describe Import::BitbucketController do
it "takes the current user's namespace" do
expect(Gitlab::BitbucketImport::ProjectCreator).
- to receive(:new).with(bitbucket_repo, user.namespace, user, access_params).
+ to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params).
and_return(double(execute: true))
post :create, format: :js
@@ -114,7 +118,7 @@ describe Import::BitbucketController do
let(:other_username) { "someone_else" }
before do
- bitbucket_repo["owner"] = other_username
+ allow(bitbucket_repo).to receive(:owner).and_return(other_username)
end
context "when a namespace with the Bitbucket user's username already exists" do
@@ -123,7 +127,7 @@ describe Import::BitbucketController do
context "when the namespace is owned by the GitLab user" do
it "takes the existing namespace" do
expect(Gitlab::BitbucketImport::ProjectCreator).
- to receive(:new).with(bitbucket_repo, existing_namespace, user, access_params).
+ to receive(:new).with(bitbucket_repo, bitbucket_repo.name, existing_namespace, user, access_params).
and_return(double(execute: true))
post :create, format: :js
@@ -156,7 +160,7 @@ describe Import::BitbucketController do
it "takes the new namespace" do
expect(Gitlab::BitbucketImport::ProjectCreator).
- to receive(:new).with(bitbucket_repo, an_instance_of(Group), user, access_params).
+ to receive(:new).with(bitbucket_repo, bitbucket_repo.name, an_instance_of(Group), user, access_params).
and_return(double(execute: true))
post :create, format: :js
@@ -177,7 +181,7 @@ describe Import::BitbucketController do
it "takes the current user's namespace" do
expect(Gitlab::BitbucketImport::ProjectCreator).
- to receive(:new).with(bitbucket_repo, user.namespace, user, access_params).
+ to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params).
and_return(double(execute: true))
post :create, format: :js
diff --git a/spec/controllers/profiles/personal_access_tokens_spec.rb b/spec/controllers/profiles/personal_access_tokens_spec.rb
new file mode 100644
index 00000000000..45534a3a587
--- /dev/null
+++ b/spec/controllers/profiles/personal_access_tokens_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Profiles::PersonalAccessTokensController do
+ let(:user) { create(:user) }
+
+ describe '#create' do
+ def created_token
+ PersonalAccessToken.order(:created_at).last
+ end
+
+ before { sign_in(user) }
+
+ it "allows creation of a token" do
+ name = FFaker::Product.brand
+
+ post :create, personal_access_token: { name: name }
+
+ expect(created_token).not_to be_nil
+ expect(created_token.name).to eq(name)
+ expect(created_token.expires_at).to be_nil
+ expect(PersonalAccessToken.active).to include(created_token)
+ end
+
+ it "allows creation of a token with an expiry date" do
+ expires_at = 5.days.from_now
+
+ post :create, personal_access_token: { name: FFaker::Product.brand, expires_at: expires_at }
+
+ expect(created_token).not_to be_nil
+ expect(created_token.expires_at.to_i).to eq(expires_at.to_i)
+ end
+
+ context "scopes" do
+ it "allows creation of a token with scopes" do
+ post :create, personal_access_token: { name: FFaker::Product.brand, scopes: ['api', 'read_user'] }
+
+ expect(created_token).not_to be_nil
+ expect(created_token.scopes).to eq(['api', 'read_user'])
+ end
+
+ it "allows creation of a token with no scopes" do
+ post :create, personal_access_token: { name: FFaker::Product.brand, scopes: [] }
+
+ expect(created_token).not_to be_nil
+ expect(created_token.scopes).to eq([])
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 52d13fb6f9e..3efef757ae2 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -36,4 +36,53 @@ describe Projects::BlobController do
end
end
end
+
+ describe 'PUT update' do
+ let(:default_params) do
+ {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: 'master/CHANGELOG',
+ target_branch: 'master',
+ content: 'Added changes',
+ commit_message: 'Update CHANGELOG'
+ }
+ end
+
+ def blob_after_edit_path
+ namespace_project_blob_path(project.namespace, project, 'master/CHANGELOG')
+ end
+
+ it 'redirects to blob' do
+ put :update, default_params
+
+ expect(response).to redirect_to(blob_after_edit_path)
+ end
+
+ context '?from_merge_request_iid' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:mr_params) { default_params.merge(from_merge_request_iid: merge_request.iid) }
+
+ it 'redirects to MR diff' do
+ put :update, mr_params
+
+ after_edit_path = diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
+ file_anchor = "##{Digest::SHA1.hexdigest('CHANGELOG')}"
+ expect(response).to redirect_to(after_edit_path + file_anchor)
+ end
+
+ context "when user doesn't have access" do
+ before do
+ other_project = create(:empty_project)
+ merge_request.update!(source_project: other_project, target_project: other_project)
+ end
+
+ it "it redirect to blob" do
+ put :update, mr_params
+
+ expect(response).to redirect_to(blob_after_edit_path)
+ end
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
index cbe0417a4a7..299d2c981d3 100644
--- a/spec/controllers/projects/boards/issues_controller_spec.rb
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -25,7 +25,7 @@ describe Projects::Boards::IssuesController do
create(:labeled_issue, project: project, labels: [planning])
create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
- issue.subscribe(johndoe)
+ issue.subscribe(johndoe, project)
list_issues user: user, board: board, list: list2
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 644de308c64..b88586b8678 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
describe Projects::BranchesController do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:developer) { create(:user) }
before do
- sign_in(user)
-
project.team << [user, :master]
+ project.team << [user, :developer]
allow(project).to receive(:branches).and_return(['master', 'foo/bar/baz'])
allow(project).to receive(:tags).and_return(['v1.0.0', 'v2.0.0'])
@@ -19,6 +19,8 @@ describe Projects::BranchesController do
context "on creation of a new branch" do
before do
+ sign_in(user)
+
post :create,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
@@ -68,6 +70,10 @@ describe Projects::BranchesController do
let(:branch) { "1-feature-branch" }
let!(:issue) { create(:issue, project: project) }
+ before do
+ sign_in(user)
+ end
+
it 'redirects' do
post :create,
namespace_id: project.namespace.to_param,
@@ -88,12 +94,34 @@ describe Projects::BranchesController do
branch_name: branch,
issue_iid: issue.iid
end
+
+ context 'without issue feature access' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
+ project.team.truncate
+ end
+
+ it "doesn't post a system note" do
+ expect(SystemNoteService).not_to receive(:new_issue_branch)
+
+ post :create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: branch,
+ issue_iid: issue.iid
+ end
+ end
end
end
describe "POST destroy with HTML format" do
render_views
+ before do
+ sign_in(user)
+ end
+
it 'returns 303' do
post :destroy,
format: :html,
@@ -109,6 +137,8 @@ describe Projects::BranchesController do
render_views
before do
+ sign_in(user)
+
post :destroy,
format: :js,
id: branch,
@@ -139,4 +169,42 @@ describe Projects::BranchesController do
it { expect(response).to have_http_status(404) }
end
end
+
+ describe "DELETE destroy_all_merged" do
+ def destroy_all_merged
+ delete :destroy_all_merged,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param
+ end
+
+ context 'when user is allowed to push' do
+ before do
+ sign_in(user)
+ end
+
+ it 'redirects to branches' do
+ destroy_all_merged
+
+ expect(response).to redirect_to namespace_project_branches_path(project.namespace, project)
+ end
+
+ it 'starts worker to delete merged branches' do
+ expect_any_instance_of(DeleteMergedBranchesService).to receive(:async_execute)
+
+ destroy_all_merged
+ end
+ end
+
+ context 'when user is not allowed to push' do
+ before do
+ sign_in(developer)
+ end
+
+ it 'responds with status 404' do
+ destroy_all_merged
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/cycle_analytics_controller_spec.rb b/spec/controllers/projects/cycle_analytics_controller_spec.rb
new file mode 100644
index 00000000000..a971adf0539
--- /dev/null
+++ b/spec/controllers/projects/cycle_analytics_controller_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Projects::CycleAnalyticsController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
+ end
+
+ describe 'cycle analytics not set up flag' do
+ context 'with no data' do
+ it 'is true' do
+ get(:show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param)
+
+ expect(response).to be_success
+ expect(assigns(:cycle_analytics_no_data)).to eq(true)
+ end
+ end
+
+ context 'with data' do
+ before do
+ issue = create(:issue, project: project, created_at: 4.days.ago)
+ milestone = create(:milestone, project: project, created_at: 5.days.ago)
+ issue.update(milestone: milestone)
+
+ create_merge_request_closing_issue(issue)
+ end
+
+ it 'is false' do
+ get(:show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param)
+
+ expect(response).to be_success
+ expect(assigns(:cycle_analytics_no_data)).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 768105cae95..bc5e2711125 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Projects::EnvironmentsController do
+ include ApiHelpers
+
let(:environment) { create(:environment) }
let(:project) { environment.project }
let(:user) { create(:user) }
@@ -11,6 +13,27 @@ describe Projects::EnvironmentsController do
sign_in(user)
end
+ describe 'GET index' do
+ context 'when standardrequest has been made' do
+ it 'responds with status code 200' do
+ get :index, environment_params
+
+ expect(response).to be_ok
+ end
+ end
+
+ context 'when requesting JSON response' do
+ it 'responds with correct JSON' do
+ get :index, environment_params(format: :json)
+
+ first_environment = json_response.first
+
+ expect(first_environment).not_to be_empty
+ expect(first_environment['name']). to eq environment.name
+ end
+ end
+ end
+
describe 'GET show' do
context 'with valid id' do
it 'responds with a status code 200' do
@@ -48,11 +71,9 @@ describe Projects::EnvironmentsController do
end
end
- def environment_params
- {
- namespace_id: project.namespace,
- project_id: project,
- id: environment.id
- }
+ def environment_params(opts = {})
+ opts.reverse_merge(namespace_id: project.namespace,
+ project_id: project,
+ id: environment.id)
end
end
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index ac3469cb8a9..028ea067a97 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -67,4 +67,62 @@ describe Projects::ForksController do
end
end
end
+
+ describe 'GET new' do
+ def get_new
+ get :new,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param
+ end
+
+ context 'when user is signed in' do
+ it 'responds with status 200' do
+ sign_in(user)
+
+ get_new
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ context 'when user is not signed in' do
+ it 'redirects to the sign-in page' do
+ sign_out(user)
+
+ get_new
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ describe 'POST create' do
+ def post_create
+ post :create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ namespace_key: user.namespace.id
+ end
+
+ context 'when user is signed in' do
+ it 'responds with status 302' do
+ sign_in(user)
+
+ post_create
+
+ expect(response).to have_http_status(302)
+ expect(response).to redirect_to(namespace_project_import_path(user.namespace, project))
+ end
+ end
+
+ context 'when user is not signed in' do
+ it 'redirects to the sign-in page' do
+ sign_out(user)
+
+ post_create
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 90419368f22..dbe5ddccbcf 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -55,6 +55,30 @@ describe Projects::IssuesController do
end
describe 'GET #new' do
+ context 'internal issue tracker' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it 'builds a new issue' do
+ get :new, namespace_id: project.namespace.path, project_id: project
+
+ expect(assigns(:issue)).to be_a_new(Issue)
+ end
+
+ it 'fills in an issue for a merge request' do
+ project_with_repository = create(:project)
+ project_with_repository.team << [user, :developer]
+ mr = create(:merge_request_with_diff_notes, source_project: project_with_repository)
+
+ get :new, namespace_id: project_with_repository.namespace.path, project_id: project_with_repository, merge_request_for_resolving_discussions: mr.iid
+
+ expect(assigns(:issue).title).not_to be_empty
+ expect(assigns(:issue).description).not_to be_empty
+ end
+ end
+
context 'external issue tracker' do
it 'redirects to the external issue tracker' do
external = double(new_issue_path: 'https://example.com/issues/new')
@@ -272,6 +296,42 @@ describe Projects::IssuesController do
end
describe 'POST #create' do
+ context 'resolving discussions in MergeRequest' 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]
+ sign_in user
+ end
+
+ let(:merge_request_params) do
+ { merge_request_for_resolving_discussions: merge_request.iid }
+ end
+
+ def post_issue(issue_params)
+ post :create, namespace_id: project.namespace.to_param, project_id: project.to_param, issue: issue_params, merge_request_for_resolving_discussions: merge_request.iid
+ end
+
+ it 'creates an issue for the project' do
+ expect { post_issue({ title: 'Hello' }) }.to change { project.issues.reload.size }.by(1)
+ end
+
+ it "doesn't overwrite given params" do
+ post_issue(description: 'Manually entered description')
+
+ expect(assigns(:issue).description).to eq('Manually entered description')
+ end
+
+ it 'resolves the discussion in the merge_request' do
+ post_issue(title: 'Hello')
+ discussion.first_note.reload
+
+ expect(discussion.resolved?).to eq(true)
+ end
+ end
+
context 'Akismet is enabled' do
before do
allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index 8faecec0063..ec6cea5c0f4 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -72,14 +72,8 @@ describe Projects::LabelsController do
end
describe 'POST #generate' do
- let(:admin) { create(:admin) }
-
- before do
- sign_in(admin)
- end
-
context 'personal project' do
- let(:personal_project) { create(:empty_project) }
+ let(:personal_project) { create(:empty_project, namespace: user.namespace) }
it 'creates labels' do
post :generate, namespace_id: personal_project.namespace.to_param, project_id: personal_project.to_param
@@ -96,4 +90,26 @@ describe Projects::LabelsController do
end
end
end
+
+ describe 'POST #toggle_subscription' do
+ it 'allows user to toggle subscription on project labels' do
+ label = create(:label, project: project)
+
+ toggle_subscription(label)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'allows user to toggle subscription on group labels' do
+ group_label = create(:group_label, group: group)
+
+ toggle_subscription(group_label)
+
+ expect(response).to have_http_status(200)
+ end
+
+ def toggle_subscription(label)
+ post :toggle_subscription, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label.to_param
+ end
+ end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 49127aecc63..9e0b80205d8 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -39,6 +39,17 @@ describe Projects::MergeRequestsController do
end
end
+ shared_examples "loads labels" do |action|
+ it "loads labels into the @labels variable" do
+ get action,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: merge_request.iid,
+ format: 'html'
+ expect(assigns(:labels)).not_to be_nil
+ end
+ end
+
describe "GET show" do
shared_examples "export merge as" do |format|
it "does generally work" do
@@ -51,6 +62,8 @@ describe Projects::MergeRequestsController do
expect(response).to be_success
end
+ it_behaves_like "loads labels", :show
+
it "generates it" do
expect_any_instance_of(MergeRequest).to receive(:"to_#{format}")
@@ -279,7 +292,9 @@ describe Projects::MergeRequestsController do
it 'sets the MR to merge when the build succeeds' do
service = double(:merge_when_build_succeeds_service)
- expect(MergeRequests::MergeWhenBuildSucceedsService).to receive(:new).with(project, anything, anything).and_return(service)
+ expect(MergeRequests::MergeWhenPipelineSucceedsService)
+ .to receive(:new).with(project, anything, anything)
+ .and_return(service)
expect(service).to receive(:execute).with(merge_request)
merge_when_build_succeeds
@@ -406,6 +421,8 @@ describe Projects::MergeRequestsController do
get :diffs, params.merge(extra_params)
end
+ it_behaves_like "loads labels", :diffs
+
context 'with default params' do
context 'as html' do
before { go(format: 'html') }
@@ -612,6 +629,8 @@ describe Projects::MergeRequestsController do
format: format
end
+ it_behaves_like "loads labels", :commits
+
context 'as html' do
it 'renders the show template' do
go
@@ -630,6 +649,14 @@ describe Projects::MergeRequestsController do
end
end
+ describe 'GET builds' do
+ it_behaves_like "loads labels", :builds
+ end
+
+ describe 'GET pipelines' do
+ it_behaves_like "loads labels", :pipelines
+ end
+
describe 'GET conflicts' do
let(:json_response) { JSON.parse(response.body) }
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 7c5f33c63b8..6d30d085056 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -31,7 +31,7 @@ describe Projects::MilestonesController do
# Check system note left for milestone removal
last_note = project.issues.find(issue.id).notes[-1].note
- expect(last_note).to eq('Milestone removed')
+ expect(last_note).to eq('removed milestone')
end
end
end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 2a7523c6512..b52137fbe7e 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -2,7 +2,7 @@ require('spec_helper')
describe Projects::ProjectMembersController do
let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
describe 'GET index' do
it 'renders index with 200 status code' do
diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb
new file mode 100644
index 00000000000..9fd5c3b85f6
--- /dev/null
+++ b/spec/controllers/projects/releases_controller_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Projects::ReleasesController do
+ let!(:project) { create(:project) }
+ let!(:user) { create(:user) }
+ let!(:release) { create(:release, project: project) }
+ let!(:tag) { release.tag }
+
+ before do
+ project.team << [user, :developer]
+ sign_in(user)
+ end
+
+ describe 'GET #edit' do
+ it 'initializes a new release' do
+ tag_id = release.tag
+ project.releases.destroy_all
+
+ get :edit, namespace_id: project.namespace.path, project_id: project.path, tag_id: tag_id
+
+ release = assigns(:release)
+ expect(release).not_to be_nil
+ expect(release).not_to be_persisted
+ end
+
+ it 'retrieves an existing release' do
+ get :edit, namespace_id: project.namespace.path, project_id: project.path, tag_id: release.tag
+
+ release = assigns(:release)
+ expect(release).not_to be_nil
+ expect(release).to be_persisted
+ end
+ end
+
+ describe 'PUT #update' do
+ it 'updates release note description' do
+ update_release('description updated')
+
+ release = project.releases.find_by_tag(tag)
+ expect(release.description).to eq("description updated")
+ end
+
+ it 'deletes release note when description is null' do
+ expect { update_release('') }.to change(project.releases, :count).by(-1)
+ end
+ end
+
+ def update_release(description)
+ put :update,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ tag_id: release.tag,
+ release: { description: description }
+ end
+end
diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todo_controller_spec.rb
index 936320a3709..415c264e0dd 100644
--- a/spec/controllers/projects/todo_controller_spec.rb
+++ b/spec/controllers/projects/todo_controller_spec.rb
@@ -4,7 +4,7 @@ describe Projects::TodosController do
include ApiHelpers
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
let(:merge_request) { create(:merge_request, source_project: project) }
@@ -42,7 +42,7 @@ describe Projects::TodosController do
end
end
- context 'when not authorized' do
+ context 'when not authorized for project' do
it 'does not create todo for issue that user has no access to' do
sign_in(user)
expect do
@@ -60,6 +60,19 @@ describe Projects::TodosController do
expect(response).to have_http_status(302)
end
end
+
+ context 'when not authorized for issue' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
+ sign_in(user)
+ end
+
+ it "doesn't create todo" do
+ expect{ go }.not_to change { user.todos.count }
+ expect(response).to have_http_status(404)
+ end
+ end
end
end
@@ -97,7 +110,7 @@ describe Projects::TodosController do
end
end
- context 'when not authorized' do
+ context 'when not authorized for project' do
it 'does not create todo for merge request user has no access to' do
sign_in(user)
expect do
@@ -115,6 +128,19 @@ describe Projects::TodosController do
expect(response).to have_http_status(302)
end
end
+
+ context 'when not authorized for merge_request' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+ sign_in(user)
+ end
+
+ it "doesn't create todo" do
+ expect{ go }.not_to change { user.todos.count }
+ expect(response).to have_http_status(404)
+ end
+ end
end
end
end
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
new file mode 100644
index 00000000000..b7bb9290712
--- /dev/null
+++ b/spec/controllers/search_controller_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe SearchController do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :public) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'finds issue comments' do
+ project = create(:empty_project, :public)
+ note = create(:note_on_issue, project: project)
+
+ get :show, project_id: project.id, scope: 'notes', search: note.note
+
+ expect(assigns[:search_objects].first).to eq note
+ end
+
+ context 'on restricted projects' do
+ context 'when signed out' do
+ before { sign_out(user) }
+
+ 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
+ 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)
+ note = create(:note_on_merge_request, 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 snippets" do
+ project = create(:empty_project, :public, snippets_access_level: ProjectFeature::PRIVATE)
+ note = create(:note_on_project_snippet, project: project)
+
+ get :show, project_id: project.id, scope: 'notes', search: note.note
+
+ expect(assigns[:search_objects].count).to eq(0)
+ end
+ end
+end
diff --git a/spec/controllers/sent_notifications_controller_spec.rb b/spec/controllers/sent_notifications_controller_spec.rb
index 191e290a118..954fc2eaf21 100644
--- a/spec/controllers/sent_notifications_controller_spec.rb
+++ b/spec/controllers/sent_notifications_controller_spec.rb
@@ -3,11 +3,11 @@ require 'rails_helper'
describe SentNotificationsController, type: :controller do
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
- let(:sent_notification) { create(:sent_notification, noteable: issue, recipient: user) }
+ let(:sent_notification) { create(:sent_notification, project: project, noteable: issue, recipient: user) }
let(:issue) do
create(:issue, project: project, author: user) do |issue|
- issue.subscriptions.create(user: user, subscribed: true)
+ issue.subscriptions.create(user: user, project: project, subscribed: true)
end
end
@@ -17,7 +17,7 @@ describe SentNotificationsController, type: :controller do
before { get(:unsubscribe, id: sent_notification.reply_key, force: true) }
it 'unsubscribes the user' do
- expect(issue.subscribed?(user)).to be_falsey
+ expect(issue.subscribed?(user, project)).to be_falsey
end
it 'sets the flash message' do
@@ -33,7 +33,7 @@ describe SentNotificationsController, type: :controller do
before { get(:unsubscribe, id: sent_notification.reply_key) }
it 'does not unsubscribe the user' do
- expect(issue.subscribed?(user)).to be_truthy
+ expect(issue.subscribed?(user, project)).to be_truthy
end
it 'does not set the flash message' do
@@ -53,7 +53,7 @@ describe SentNotificationsController, type: :controller do
before { get(:unsubscribe, id: sent_notification.reply_key.reverse) }
it 'does not unsubscribe the user' do
- expect(issue.subscribed?(user)).to be_truthy
+ expect(issue.subscribed?(user, project)).to be_truthy
end
it 'does not set the flash message' do
@@ -69,7 +69,7 @@ describe SentNotificationsController, type: :controller do
before { get(:unsubscribe, id: sent_notification.reply_key, force: true) }
it 'unsubscribes the user' do
- expect(issue.subscribed?(user)).to be_falsey
+ expect(issue.subscribed?(user, project)).to be_falsey
end
it 'sets the flash message' do
@@ -85,14 +85,14 @@ describe SentNotificationsController, type: :controller do
context 'when the force param is not passed' do
let(:merge_request) do
create(:merge_request, source_project: project, author: user) do |merge_request|
- merge_request.subscriptions.create(user: user, subscribed: true)
+ merge_request.subscriptions.create(user: user, project: project, subscribed: true)
end
end
- let(:sent_notification) { create(:sent_notification, noteable: merge_request, recipient: user) }
+ let(:sent_notification) { create(:sent_notification, project: project, noteable: merge_request, recipient: user) }
before { get(:unsubscribe, id: sent_notification.reply_key) }
it 'unsubscribes the user' do
- expect(merge_request.subscribed?(user)).to be_falsey
+ expect(merge_request.subscribed?(user, project)).to be_falsey
end
it 'sets the flash message' do
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 48d69377461..b56c7880b64 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -22,7 +22,6 @@ describe SessionsController do
it 'authenticates user correctly' do
post(:create, user: { login: user.username, password: user.password })
- expect(response).to set_flash.to /Signed in successfully/
expect(subject.current_user). to eq user
end
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 2d762fdaa04..d76fe9f580f 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -3,6 +3,28 @@ require 'spec_helper'
describe SnippetsController do
let(:user) { create(:user) }
+ describe 'GET #new' do
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ it 'responds with status 200' do
+ get :new
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get :new
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
describe 'GET #show' do
context 'when the personal snippet is private' do
let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
diff --git a/spec/db/production/settings.rb b/spec/db/production/settings.rb
new file mode 100644
index 00000000000..a7c5283df94
--- /dev/null
+++ b/spec/db/production/settings.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+require 'rainbow/ext/string'
+
+describe 'seed production settings', lib: true do
+ context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do
+ before do
+ allow(ENV).to receive(:[]).and_call_original
+ allow(ENV).to receive(:[]).with('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN').and_return('013456789')
+ end
+
+ it 'writes the token to the database' do
+ load(File.join(__dir__, '../../../db/fixtures/production/010_settings.rb'))
+ expect(ApplicationSetting.current.runners_registration_token).to eq('013456789')
+ end
+ end
+end
diff --git a/spec/factories/chat_names.rb b/spec/factories/chat_names.rb
new file mode 100644
index 00000000000..24225468d55
--- /dev/null
+++ b/spec/factories/chat_names.rb
@@ -0,0 +1,16 @@
+FactoryGirl.define do
+ factory :chat_name, class: ChatName do
+ user factory: :user
+ service factory: :service
+
+ team_id 'T0001'
+ team_domain 'Awesome Team'
+
+ sequence :chat_id do |n|
+ "U#{n}"
+ end
+ sequence :chat_name do |n|
+ "user#{n}"
+ end
+ end
+end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 0c93bbdfe26..62466c06194 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -12,12 +12,14 @@ FactoryGirl.define do
started_at 'Di 29. Okt 09:51:28 CET 2013'
finished_at 'Di 29. Okt 09:53:28 CET 2013'
commands 'ls -a'
+
options do
{
image: "ruby:2.1",
services: ["postgres"]
}
end
+
yaml_variables do
[
{ key: :DB_NAME, value: 'postgres', public: true }
@@ -38,6 +40,10 @@ FactoryGirl.define do
status 'canceled'
end
+ trait :skipped do
+ status 'skipped'
+ end
+
trait :running do
status 'running'
end
@@ -55,10 +61,21 @@ FactoryGirl.define do
self.when 'manual'
end
+ trait :teardown_environment do
+ environment 'staging'
+ options environment: { name: 'staging',
+ action: 'stop' }
+ end
+
trait :allowed_to_fail do
allow_failure true
end
+ trait :playable do
+ skipped
+ manual
+ end
+
after(:build) do |build, evaluator|
build.project = build.pipeline.project
end
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index ac2a1ba5dff..1735791f644 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -7,26 +7,30 @@ FactoryGirl.define do
project factory: :empty_project
factory :ci_pipeline_without_jobs do
- after(:build) do |commit|
- allow(commit).to receive(:ci_yaml_file) { YAML.dump({}) }
+ after(:build) do |pipeline|
+ allow(pipeline).to receive(:ci_yaml_file) { YAML.dump({}) }
end
end
factory :ci_pipeline_with_one_job do
- after(:build) do |commit|
- allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" } }) }
- end
- end
-
- factory :ci_pipeline_with_two_job do
- after(:build) do |commit|
- allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" }, spinach: { script: "ls" } }) }
+ after(:build) do |pipeline|
+ allow(pipeline).to receive(:ci_yaml_file) do
+ YAML.dump({ rspec: { script: "ls" } })
+ end
end
end
factory :ci_pipeline do
- after(:build) do |commit|
- allow(commit).to receive(:ci_yaml_file) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
+ transient { config nil }
+
+ after(:build) do |pipeline, evaluator|
+ allow(pipeline).to receive(:ci_yaml_file) do
+ if evaluator.config
+ YAML.dump(evaluator.config)
+ else
+ File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+ end
+ end
end
end
end
diff --git a/spec/factories/ci/stages.rb b/spec/factories/ci/stages.rb
new file mode 100644
index 00000000000..ee3b17b8bf1
--- /dev/null
+++ b/spec/factories/ci/stages.rb
@@ -0,0 +1,13 @@
+FactoryGirl.define do
+ factory :ci_stage, class: Ci::Stage do
+ transient do
+ name 'test'
+ status nil
+ pipeline factory: :ci_empty_pipeline
+ end
+
+ initialize_with do
+ Ci::Stage.new(pipeline, name: name, status: status)
+ end
+ end
+end
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index 995f2080f10..756b341ecba 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -19,6 +19,10 @@ FactoryGirl.define do
status 'canceled'
end
+ trait :skipped do
+ status 'skipped'
+ end
+
trait :running do
status 'running'
end
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
index 6f24bf58d14..29ad1af9fd9 100644
--- a/spec/factories/deployments.rb
+++ b/spec/factories/deployments.rb
@@ -3,8 +3,9 @@ FactoryGirl.define do
sha '97de212e80737a608d939f648d959671fb0a0142'
ref 'master'
tag false
+ user
project nil
-
+ deployable factory: :ci_build
environment factory: :environment
after(:build) do |deployment, evaluator|
diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb
index 846cccfc7fa..0852dda6b29 100644
--- a/spec/factories/environments.rb
+++ b/spec/factories/environments.rb
@@ -4,5 +4,33 @@ FactoryGirl.define do
project factory: :empty_project
sequence(:external_url) { |n| "https://env#{n}.example.gitlab.com" }
+
+ trait :with_review_app do |environment|
+ project
+
+ transient do
+ ref 'master'
+ end
+
+ # At this point `review app` is an ephemeral concept related to
+ # deployments being deployed for given environment. There is no
+ # first-class `review app` available so we need to create set of
+ # interconnected objects to simulate a review app.
+ #
+ after(:create) do |environment, evaluator|
+ deployment = create(:deployment,
+ environment: environment,
+ project: environment.project,
+ ref: evaluator.ref,
+ sha: environment.project.commit(evaluator.ref).id)
+
+ teardown_build = create(:ci_build, :manual,
+ name: "#{deployment.environment.name}:teardown",
+ pipeline: deployment.deployable.pipeline)
+
+ deployment.update_column(:on_stop, teardown_build.name)
+ environment.update_attribute(:deployments, [deployment])
+ end
+ end
end
end
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 2d47a6f6c4c..ece6beb9fa9 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -15,5 +15,13 @@ FactoryGirl.define do
trait :private do
visibility_level Gitlab::VisibilityLevel::PRIVATE
end
+
+ trait :access_requestable do
+ request_access_enabled true
+ end
+
+ trait :nested do
+ parent factory: :group
+ end
end
end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 6919002dedc..a10ba629760 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -67,7 +67,7 @@ FactoryGirl.define do
end
trait :on_project_snippet do
- noteable { create(:snippet, project: project) }
+ noteable { create(:project_snippet, project: project) }
end
trait :system do
diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb
index da4c72bcb5b..811eab7e15b 100644
--- a/spec/factories/personal_access_tokens.rb
+++ b/spec/factories/personal_access_tokens.rb
@@ -5,5 +5,6 @@ FactoryGirl.define do
name { FFaker::Product.brand }
revoked false
expires_at { 5.days.from_now }
+ scopes ['api']
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index dd4a86b1e31..0d072d6a690 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -24,6 +24,10 @@ FactoryGirl.define do
visibility_level Gitlab::VisibilityLevel::PRIVATE
end
+ trait :access_requestable do
+ request_access_enabled true
+ end
+
trait :empty_repo do
after(:create) do |project|
project.create_repository
@@ -49,13 +53,17 @@ FactoryGirl.define do
end
after(:create) do |project, evaluator|
+ # Builds and MRs can't have higher visibility level than repository access level.
+ builds_access_level = [evaluator.builds_access_level, evaluator.repository_access_level].min
+ merge_requests_access_level = [evaluator.merge_requests_access_level, evaluator.repository_access_level].min
+
project.project_feature.
- update_attributes(
+ update_attributes!(
wiki_access_level: evaluator.wiki_access_level,
- builds_access_level: evaluator.builds_access_level,
+ builds_access_level: builds_access_level,
snippets_access_level: evaluator.snippets_access_level,
issues_access_level: evaluator.issues_access_level,
- merge_requests_access_level: evaluator.merge_requests_access_level,
+ merge_requests_access_level: merge_requests_access_level,
repository_access_level: evaluator.repository_access_level
)
end
@@ -125,4 +133,17 @@ FactoryGirl.define do
)
end
end
+
+ factory :kubernetes_project, parent: :empty_project do
+ after :create do |project|
+ project.create_kubernetes_service(
+ active: true,
+ properties: {
+ namespace: project.path,
+ api_url: 'https://kubernetes.example.com/api',
+ token: 'a' * 40,
+ }
+ )
+ end
+ end
end
diff --git a/spec/factories/subscriptions.rb b/spec/factories/subscriptions.rb
new file mode 100644
index 00000000000..b11b0a0a17b
--- /dev/null
+++ b/spec/factories/subscriptions.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :subscription do
+ user
+ project factory: :empty_project
+ subscribable factory: :issue
+ end
+end
diff --git a/spec/features/abuse_report_spec.rb b/spec/features/abuse_report_spec.rb
new file mode 100644
index 00000000000..1e11fb756b2
--- /dev/null
+++ b/spec/features/abuse_report_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+feature 'Abuse reports', feature: true do
+ let(:another_user) { create(:user) }
+
+ before do
+ login_as :user
+ end
+
+ scenario 'Report abuse' do
+ visit user_path(another_user)
+
+ click_link 'Report abuse'
+
+ fill_in 'abuse_report_message', with: 'This user send spam'
+ click_button 'Send report'
+
+ expect(page).to have_content 'Thank you for your report'
+
+ visit user_path(another_user)
+
+ expect(page).to have_button("Already reported for abuse")
+ end
+end
diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb
index c1731e6414a..7fcfe5a54c7 100644
--- a/spec/features/admin/admin_abuse_reports_spec.rb
+++ b/spec/features/admin/admin_abuse_reports_spec.rb
@@ -4,17 +4,21 @@ describe "Admin::AbuseReports", feature: true, js: true do
let(:user) { create(:user) }
context 'as an admin' do
+ before do
+ login_as :admin
+ end
+
describe 'if a user has been reported for abuse' do
- before do
- create(:abuse_report, user: user)
- login_as :admin
- end
+ let!(:abuse_report) { create(:abuse_report, user: user) }
describe 'in the abuse report view' do
- it "presents a link to the user's profile" do
+ it 'presents information about abuse report' do
visit admin_abuse_reports_path
- expect(page).to have_link user.name, href: user_path(user)
+ expect(page).to have_content('Abuse Reports')
+ expect(page).to have_content(abuse_report.message)
+ expect(page).to have_link(user.name, href: user_path(user))
+ expect(page).to have_link('Remove user')
end
end
diff --git a/spec/features/admin/admin_active_tab_spec.rb b/spec/features/admin/admin_active_tab_spec.rb
new file mode 100644
index 00000000000..16064d60ce2
--- /dev/null
+++ b/spec/features/admin/admin_active_tab_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+RSpec.describe 'admin active tab' do
+ before do
+ login_as :admin
+ end
+
+ shared_examples 'page has active tab' do |title|
+ it "activates #{title} tab" do
+ expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 1)
+ expect(page.find('.layout-nav li.active')).to have_content(title)
+ end
+ end
+
+ shared_examples 'page has active sub tab' do |title|
+ it "activates #{title} sub tab" do
+ expect(page).to have_selector('.sub-nav li.active', count: 1)
+ expect(page.find('.sub-nav li.active')).to have_content(title)
+ end
+ end
+
+ context 'on home page' do
+ before do
+ visit admin_root_path
+ end
+
+ it_behaves_like 'page has active tab', 'Overview'
+ end
+
+ context 'on projects' do
+ before do
+ visit admin_projects_path
+ end
+
+ it_behaves_like 'page has active tab', 'Overview'
+ it_behaves_like 'page has active sub tab', 'Projects'
+ end
+
+ context 'on groups' do
+ before do
+ visit admin_groups_path
+ end
+
+ it_behaves_like 'page has active tab', 'Overview'
+ it_behaves_like 'page has active sub tab', 'Groups'
+ end
+
+ context 'on users' do
+ before do
+ visit admin_users_path
+ end
+
+ it_behaves_like 'page has active tab', 'Overview'
+ it_behaves_like 'page has active sub tab', 'Users'
+ end
+
+ context 'on logs' do
+ before do
+ visit admin_logs_path
+ end
+
+ it_behaves_like 'page has active tab', 'Monitoring'
+ it_behaves_like 'page has active sub tab', 'Logs'
+ end
+
+ context 'on messages' do
+ before do
+ visit admin_broadcast_messages_path
+ end
+
+ it_behaves_like 'page has active tab', 'Messages'
+ end
+
+ context 'on hooks' do
+ before do
+ visit admin_hooks_path
+ end
+
+ it_behaves_like 'page has active tab', 'Hooks'
+ end
+
+ context 'on background jobs' do
+ before do
+ visit admin_background_jobs_path
+ end
+
+ it_behaves_like 'page has active tab', 'Monitoring'
+ it_behaves_like 'page has active sub tab', 'Background Jobs'
+ end
+end
diff --git a/spec/features/admin/admin_browse_spam_logs_spec.rb b/spec/features/admin/admin_browse_spam_logs_spec.rb
new file mode 100644
index 00000000000..562ace92598
--- /dev/null
+++ b/spec/features/admin/admin_browse_spam_logs_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe 'Admin browse spam logs' do
+ let!(:spam_log) { create(:spam_log) }
+
+ before do
+ login_as :admin
+ end
+
+ scenario 'Browse spam logs' do
+ visit admin_spam_logs_path
+
+ expect(page).to have_content('Spam Logs')
+ expect(page).to have_content(spam_log.source_ip)
+ expect(page).to have_content(spam_log.noteable_type)
+ expect(page).to have_content('N')
+ expect(page).to have_content(spam_log.title)
+ expect(page).to have_content("#{spam_log.description[0...97]}...")
+ expect(page).to have_link('Remove user')
+ expect(page).to have_link('Block user')
+ end
+end
diff --git a/spec/features/admin/admin_browses_logs_spec.rb b/spec/features/admin/admin_browses_logs_spec.rb
new file mode 100644
index 00000000000..d880f3f07db
--- /dev/null
+++ b/spec/features/admin/admin_browses_logs_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'Admin browses logs' do
+ before do
+ login_as :admin
+ end
+
+ it 'shows available log files' do
+ visit admin_logs_path
+
+ expect(page).to have_content 'test.log'
+ expect(page).to have_content 'githost.log'
+ expect(page).to have_content 'application.log'
+ end
+end
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
new file mode 100644
index 00000000000..0aa01fc499a
--- /dev/null
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+feature 'Admin Groups', feature: true do
+ let(:internal) { Gitlab::VisibilityLevel::INTERNAL }
+
+ before do
+ login_as(:admin)
+
+ stub_application_setting(default_group_visibility: internal)
+ end
+
+ describe 'create a group' do
+ scenario 'shows the visibility level radio populated with the default value' do
+ visit new_admin_group_path
+
+ expect_selected_visibility(internal)
+ end
+ end
+
+ describe 'group edit' do
+ scenario 'shows the visibility level radio populated with the group visibility_level value' do
+ group = create(:group, :private)
+
+ visit admin_group_edit_path(group)
+
+ expect_selected_visibility(group.visibility_level)
+ end
+ end
+
+ def expect_selected_visibility(level)
+ selector = "#group_visibility_level_#{level}[checked=checked]"
+
+ expect(page).to have_selector(selector, count: 1)
+ end
+end
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index b3ce72b1452..f246997d5a2 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -26,16 +26,17 @@ describe "Admin::Hooks", feature: true do
end
describe "New Hook" do
- before do
- @url = FFaker::Internet.uri("http")
+ let(:url) { FFaker::Internet.uri('http') }
+
+ it 'adds new hook' do
visit admin_hooks_path
- fill_in "hook_url", with: @url
- expect { click_button "Add System Hook" }.to change(SystemHook, :count).by(1)
- end
+ fill_in 'hook_url', with: url
+ check 'Enable SSL verification'
- it "opens new hook popup" do
+ expect { click_button 'Add System Hook' }.to change(SystemHook, :count).by(1)
+ expect(page).to have_content 'SSL Verification: enabled'
expect(current_path).to eq(admin_hooks_path)
- expect(page).to have_content(@url)
+ expect(page).to have_content(url)
end
end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index 30ded9202a4..a36bfd574cb 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -8,11 +8,11 @@ describe "Admin::Projects", feature: true do
describe "GET /admin/projects" do
before do
- visit admin_namespaces_projects_path
+ visit admin_projects_path
end
it "is ok" do
- expect(current_path).to eq(admin_namespaces_projects_path)
+ expect(current_path).to eq(admin_projects_path)
end
it "has projects list" do
@@ -22,7 +22,7 @@ describe "Admin::Projects", feature: true do
describe "GET /admin/projects/:id" do
before do
- visit admin_namespaces_projects_path
+ visit admin_projects_path
click_link "#{@project.name}"
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
new file mode 100644
index 00000000000..8cd66f189be
--- /dev/null
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+feature 'Admin updates settings', feature: true do
+ before(:each) do
+ login_as :admin
+ visit admin_application_settings_path
+ end
+
+ scenario 'Change application settings' do
+ uncheck 'Gravatar enabled'
+ fill_in 'Home page URL', with: 'https://about.gitlab.com/'
+ fill_in 'Help page text', with: 'Example text'
+ click_button 'Save'
+
+ expect(current_application_settings.gravatar_enabled).to be_falsey
+ expect(current_application_settings.home_page_url).to eq "https://about.gitlab.com/"
+ expect(page).to have_content "Application settings saved successfully"
+ end
+
+ scenario 'Change Slack Service template settings' do
+ click_link 'Service Templates'
+ click_link 'Slack'
+ fill_in 'Webhook', with: 'http://localhost'
+ fill_in 'Username', with: 'test_user'
+ fill_in 'service_push_channel', with: '#test_channel'
+ page.check('Notify only broken builds')
+
+ check_all_events
+ click_on 'Save'
+
+ expect(page).to have_content 'Application settings saved successfully'
+
+ click_link 'Slack'
+
+ page.all('input[type=checkbox]').each do |checkbox|
+ expect(checkbox).to be_checked
+ end
+ expect(find_field('Webhook').value).to eq 'http://localhost'
+ expect(find_field('Username').value).to eq 'test_user'
+ expect(find('#service_push_channel').value).to eq '#test_channel'
+ end
+
+ def check_all_events
+ page.check('Active')
+ page.check('Push')
+ page.check('Tag push')
+ page.check('Note')
+ page.check('Issue')
+ page.check('Merge request')
+ page.check('Build')
+ page.check('Pipeline')
+ end
+end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index cb3191dfdde..e31325ce47b 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -225,4 +225,32 @@ describe "Admin::Users", feature: true do
end
end
end
+
+ describe "GET /admin/users/:id/projects" do
+ before do
+ @group = create(:group)
+ @project = create(:project, group: @group)
+ @simple_user = create(:user)
+ @group.add_developer(@simple_user)
+
+ visit projects_admin_user_path(@simple_user)
+ end
+
+ it "lists group projects" do
+ within(:css, '.append-bottom-default + .panel') do
+ expect(page).to have_content 'Group projects'
+ expect(page).to have_link @group.name, admin_group_path(@group)
+ end
+ end
+
+ it 'allows navigation to the group details' do
+ within(:css, '.append-bottom-default + .panel') do
+ click_link @group.name
+ end
+ within(:css, 'h3.page-title') do
+ expect(page).to have_content "Group: #{@group.name}"
+ end
+ expect(page).to have_content @project.name
+ end
+ end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 6cb8753e8fc..973d5b286e9 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -158,7 +158,7 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'removes checkmark in new list dropdown after deleting' do
- click_button 'Create new list'
+ click_button 'Add list'
wait_for_ajax
page.within(find('.board:nth-child(2)')) do
@@ -304,7 +304,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'new list' do
it 'shows all labels in new list dropdown' do
- click_button 'Create new list'
+ click_button 'Add list'
wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do
@@ -315,7 +315,7 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'creates new list for label' do
- click_button 'Create new list'
+ click_button 'Add list'
wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do
@@ -328,7 +328,7 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'creates new list for Backlog label' do
- click_button 'Create new list'
+ click_button 'Add list'
wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do
@@ -341,7 +341,7 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'creates new list for Done label' do
- click_button 'Create new list'
+ click_button 'Add list'
wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do
@@ -354,7 +354,7 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'keeps dropdown open after adding new list' do
- click_button 'Create new list'
+ click_button 'Add list'
wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do
@@ -369,7 +369,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'moves issues from backlog into new list' do
wait_for_board_cards(1, 6)
- click_button 'Create new list'
+ click_button 'Add list'
wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do
@@ -382,7 +382,7 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'creates new list from a new label' do
- click_button 'Create new list'
+ click_button 'Add list'
wait_for_ajax
@@ -659,6 +659,10 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
end
+ it 'displays lists' do
+ expect(page).to have_selector('.board')
+ end
+
it 'does not show create new list' do
expect(page).not_to have_selector('.js-new-board-list')
end
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index 760a8967123..a03cd6fbf2d 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -46,7 +46,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
click_button 'Cancel'
- expect(page).to have_selector('.board-new-issue-form', visible: false)
+ expect(page).not_to have_selector('.board-new-issue-form')
end
end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index f160052a844..c16aafa1470 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -304,8 +304,8 @@ describe 'Issue Boards', feature: true, js: true do
page.within('.subscription') do
click_button 'Subscribe'
-
- expect(page).to have_content("You're receiving notifications because you're subscribed to this thread.")
+ wait_for_ajax
+ expect(page).to have_content("Unsubscribe")
end
end
end
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index 7fa0c95cae2..3e0b6364e0d 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -52,6 +52,10 @@ feature 'Contributions Calendar', js: true, feature: true do
Event.create(push_params)
end
+ def get_first_cell_content
+ find('.user-calendar-activities').text
+ end
+
before do
login_as :user
visit @user.username
@@ -62,6 +66,43 @@ feature 'Contributions Calendar', js: true, feature: true do
expect(page).to have_css('.js-contrib-calendar')
end
+ describe 'select calendar day', js: true do
+ let(:cells) { page.all('.user-contrib-cell') }
+ let(:first_cell_content_before) { get_first_cell_content }
+
+ before do
+ cells[0].click
+ wait_for_ajax
+ first_cell_content_before
+ end
+
+ it 'displays calendar day activities', js: true do
+ expect(get_first_cell_content).not_to eq('')
+ end
+
+ describe 'select another calendar day', js: true do
+ before do
+ cells[1].click
+ wait_for_ajax
+ end
+
+ it 'displays different calendar day activities', js: true do
+ expect(get_first_cell_content).not_to eq(first_cell_content_before)
+ end
+ end
+
+ describe 'deselect calendar day', js: true do
+ before do
+ cells[0].click
+ wait_for_ajax
+ end
+
+ it 'hides calendar day activities', js: true do
+ expect(get_first_cell_content).to eq('')
+ end
+ end
+ end
+
describe '1 calendar activity' do
before do
Issues::CreateService.new(contributed_project, @user, issue_params).execute
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 44646ffc602..8f561c8f90b 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -107,7 +107,7 @@ describe 'Commits' do
describe 'Cancel build' do
it 'cancels build' do
visit ci_status_path(pipeline)
- click_on 'Cancel'
+ find('a.btn[title="Cancel"]').click
expect(page).to have_content 'canceled'
end
end
@@ -177,4 +177,23 @@ describe 'Commits' do
end
end
end
+
+ context 'viewing commits for a branch' do
+ let(:branch_name) { 'master' }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ login_with(user)
+ visit namespace_project_commits_path(project.namespace, project, branch_name)
+ end
+
+ it 'includes the committed_date for each commit' do
+ commits = project.repository.commits(branch_name)
+
+ commits.each do |commit|
+ expect(page).to have_content("committed #{commit.committed_date}")
+ end
+ end
+ end
end
diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb
index 365cb445df1..44dfc2dff45 100644
--- a/spec/features/dashboard/datetime_on_tooltips_spec.rb
+++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb
@@ -36,7 +36,7 @@ feature 'Tooltips on .timeago dates', feature: true, js: true do
visit user_snippets_path(user)
wait_for_ajax()
- page.find('.js-timeago').hover
+ page.find('.js-timeago.snippet-created-ago').hover
end
it 'has the datetime formated correctly' do
diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb
new file mode 100644
index 00000000000..41dcfe439c2
--- /dev/null
+++ b/spec/features/dashboard/issuables_counter_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe 'Navigation bar counter', feature: true, js: true, caching: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
+ let(:issue) { create(:issue, project: project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ before do
+ issue.update(assignee: user)
+ merge_request.update(assignee: user)
+ login_as(user)
+ end
+
+ it 'reflects dashboard issues count' do
+ visit issues_dashboard_path
+
+ expect_counters('issues', '1')
+
+ issue.update(assignee: nil)
+ visit issues_dashboard_path
+
+ expect_counters('issues', '1')
+ end
+
+ it 'reflects dashboard merge requests count' do
+ visit merge_requests_dashboard_path
+
+ expect_counters('merge_requests', '1')
+
+ merge_request.update(assignee: nil)
+ visit merge_requests_dashboard_path
+
+ expect_counters('merge_requests', '1')
+ end
+
+ def expect_counters(issuable_type, count)
+ dashboard_count = find('li.active span.badge')
+ nav_count = find(".dashboard-shortcuts-#{issuable_type} span.count")
+
+ expect(nav_count).to have_content(count)
+ expect(dashboard_count).to have_content(count)
+ end
+end
diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb
new file mode 100644
index 00000000000..0c1939fd885
--- /dev/null
+++ b/spec/features/environment_spec.rb
@@ -0,0 +1,161 @@
+require 'spec_helper'
+
+feature 'Environment', :feature do
+ given(:project) { create(:empty_project) }
+ given(:user) { create(:user) }
+ given(:role) { :developer }
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ end
+
+ feature 'environment details page' do
+ given!(:environment) { create(:environment, project: project) }
+ given!(:deployment) { }
+ given!(:manual) { }
+
+ before do
+ visit_environment(environment)
+ end
+
+ context 'without deployments' do
+ scenario 'does show no deployments' do
+ expect(page).to have_content('You don\'t have any deployments right now.')
+ end
+ end
+
+ context 'with deployments' do
+ context 'when there is no related deployable' do
+ given(:deployment) do
+ create(:deployment, environment: environment, deployable: nil)
+ end
+
+ scenario 'does show deployment SHA' do
+ expect(page).to have_link(deployment.short_sha)
+ end
+
+ scenario 'does not show a re-deploy button for deployment without build' do
+ expect(page).not_to have_link('Re-deploy')
+ end
+ end
+
+ context 'with related deployable present' do
+ given(:pipeline) { create(:ci_pipeline, project: project) }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+
+ given(:deployment) do
+ create(:deployment, environment: environment, deployable: build)
+ end
+
+ scenario 'does show build name' do
+ expect(page).to have_link("#{build.name} (##{build.id})")
+ end
+
+ scenario 'does show re-deploy button' do
+ expect(page).to have_link('Re-deploy')
+ end
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_link('Stop')
+ end
+
+ context 'with manual action' do
+ given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
+
+ scenario 'does show a play button' do
+ expect(page).to have_link(manual.name.humanize)
+ end
+
+ scenario 'does allow to play manual action' do
+ expect(manual).to be_skipped
+ expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count }
+ expect(page).to have_content(manual.name)
+ expect(manual.reload).to be_pending
+ end
+
+ context 'with external_url' do
+ given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+
+ scenario 'does show an external link button' do
+ expect(page).to have_link(nil, href: environment.external_url)
+ end
+ end
+
+ context 'with stop action' do
+ given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+
+ scenario 'does show stop button' do
+ expect(page).to have_link('Stop')
+ end
+
+ scenario 'does allow to stop environment' do
+ click_link('Stop')
+
+ expect(page).to have_content('close_app')
+ end
+
+ context 'for reporter' do
+ let(:role) { :reporter }
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_link('Stop')
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ feature 'auto-close environment when branch is deleted' do
+ given(:project) { create(:project) }
+
+ given!(:environment) do
+ create(:environment, :with_review_app, project: project,
+ ref: 'feature')
+ end
+
+ scenario 'user visits environment page' do
+ visit_environment(environment)
+
+ expect(page).to have_link('Stop')
+ end
+
+ scenario 'user deletes the branch with running environment' do
+ visit namespace_project_branches_path(project.namespace, project)
+
+ remove_branch_with_hooks(project, user, 'feature') do
+ page.within('.js-branch-feature') { find('a.btn-remove').click }
+ end
+
+ visit_environment(environment)
+
+ expect(page).to have_no_link('Stop')
+ end
+
+ ##
+ # This is a workaround for problem described in #24543
+ #
+ def remove_branch_with_hooks(project, user, branch)
+ params = {
+ oldrev: project.commit(branch).id,
+ newrev: Gitlab::Git::BLANK_SHA,
+ ref: "refs/heads/#{branch}"
+ }
+
+ yield
+
+ GitPushService.new(project, user, params).execute
+ end
+ end
+
+ def visit_environment(environment)
+ visit namespace_project_environment_path(environment.project.namespace,
+ environment.project,
+ environment)
+ end
+end
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
index b565586ee14..e1b97b31e5d 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/environments_spec.rb
@@ -1,228 +1,147 @@
require 'spec_helper'
-feature 'Environments', feature: true do
+feature 'Environments page', :feature, :js do
given(:project) { create(:empty_project) }
given(:user) { create(:user) }
given(:role) { :developer }
background do
- login_as(user)
project.team << [user, role]
+ login_as(user)
end
- describe 'when showing environments' do
- given!(:environment) { }
- given!(:deployment) { }
- given!(:manual) { }
-
- before do
- visit namespace_project_environments_path(project.namespace, project)
- end
+ given!(:environment) { }
+ given!(:deployment) { }
+ given!(:manual) { }
- context 'shows two tabs' do
- scenario 'shows "Available" and "Stopped" tab with links' do
- expect(page).to have_link('Available')
- expect(page).to have_link('Stopped')
- end
- end
-
- context 'without environments' do
- scenario 'does show no environments' do
- expect(page).to have_content('You don\'t have any environments right now.')
- end
+ before do
+ visit_environments(project)
+ end
- scenario 'does show 0 as counter for environments in both tabs' do
- expect(page.find('.js-available-environments-count').text).to eq('0')
- expect(page.find('.js-stopped-environments-count').text).to eq('0')
- end
+ describe 'page tabs' do
+ scenario 'shows "Available" and "Stopped" tab with links' do
+ expect(page).to have_link('Available')
+ expect(page).to have_link('Stopped')
end
+ end
- context 'with environments' do
- given(:environment) { create(:environment, project: project) }
-
- scenario 'does show environment name' do
- expect(page).to have_link(environment.name)
- end
-
- scenario 'does show number of available and stopped environments' do
- expect(page.find('.js-available-environments-count').text).to eq('1')
- expect(page.find('.js-stopped-environments-count').text).to eq('0')
- end
-
- context 'without deployments' do
- scenario 'does show no deployments' do
- expect(page).to have_content('No deployments yet')
- end
- end
-
- context 'with deployments' do
- given(:deployment) { create(:deployment, environment: environment) }
-
- scenario 'does show deployment SHA' do
- expect(page).to have_link(deployment.short_sha)
- end
-
- scenario 'does show deployment internal id' do
- expect(page).to have_content(deployment.iid)
- end
-
- context 'with build and manual actions' do
- given(:pipeline) { create(:ci_pipeline, project: project) }
- given(:build) { create(:ci_build, pipeline: pipeline) }
- given(:deployment) { create(:deployment, environment: environment, deployable: build) }
- given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
-
- scenario 'does show a play button' do
- expect(page).to have_link(manual.name.humanize)
- end
-
- scenario 'does allow to play manual action' do
- expect(manual).to be_skipped
- expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count }
- expect(page).to have_content(manual.name)
- expect(manual.reload).to be_pending
- end
-
- scenario 'does show build name and id' do
- expect(page).to have_link("#{build.name} (##{build.id})")
- end
-
- scenario 'does not show stop button' do
- expect(page).not_to have_selector('.stop-env-link')
- end
-
- scenario 'does not show external link button' do
- expect(page).not_to have_css('external-url')
- end
-
- context 'with external_url' do
- given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
- given(:build) { create(:ci_build, pipeline: pipeline) }
- given(:deployment) { create(:deployment, environment: environment, deployable: build) }
-
- scenario 'does show an external link button' do
- expect(page).to have_link(nil, href: environment.external_url)
- end
- end
-
- context 'with stop action' do
- given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
- given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
-
- scenario 'does show stop button' do
- expect(page).to have_selector('.stop-env-link')
- end
-
- scenario 'starts build when stop button clicked' do
- first('.stop-env-link').click
-
- expect(page).to have_content('close_app')
- end
-
- context 'for reporter' do
- let(:role) { :reporter }
-
- scenario 'does not show stop button' do
- expect(page).not_to have_selector('.stop-env-link')
- end
- end
- end
- end
- end
+ context 'without environments' do
+ scenario 'does show no environments' do
+ expect(page).to have_content('You don\'t have any environments right now.')
end
- scenario 'does have a New environment button' do
- expect(page).to have_link('New environment')
+ scenario 'does show 0 as counter for environments in both tabs' do
+ expect(page.find('.js-available-environments-count').text).to eq('0')
+ expect(page.find('.js-stopped-environments-count').text).to eq('0')
end
end
describe 'when showing the environment' do
given(:environment) { create(:environment, project: project) }
- given!(:deployment) { }
- given!(:manual) { }
- before do
- visit namespace_project_environment_path(project.namespace, project, environment)
+ scenario 'does show environment name' do
+ expect(page).to have_link(environment.name)
+ end
+
+ scenario 'does show number of available and stopped environments' do
+ expect(page.find('.js-available-environments-count').text).to eq('1')
+ expect(page.find('.js-stopped-environments-count').text).to eq('0')
end
context 'without deployments' do
scenario 'does show no deployments' do
- expect(page).to have_content('You don\'t have any deployments right now.')
+ expect(page).to have_content('No deployments yet')
end
end
context 'with deployments' do
- given(:deployment) { create(:deployment, environment: environment) }
+ given(:project) { create(:project) }
+
+ given(:deployment) do
+ create(:deployment, environment: environment,
+ sha: project.commit.id)
+ end
scenario 'does show deployment SHA' do
expect(page).to have_link(deployment.short_sha)
end
- scenario 'does not show a re-deploy button for deployment without build' do
- expect(page).not_to have_link('Re-deploy')
+ scenario 'does show deployment internal id' do
+ expect(page).to have_content(deployment.iid)
end
- context 'with build' do
+ context 'with build and manual actions' do
given(:pipeline) { create(:ci_pipeline, project: project) }
given(:build) { create(:ci_build, pipeline: pipeline) }
- given(:deployment) { create(:deployment, environment: environment, deployable: build) }
- scenario 'does show build name' do
- expect(page).to have_link("#{build.name} (##{build.id})")
+ given(:manual) do
+ create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production')
end
- scenario 'does show re-deploy button' do
- expect(page).to have_link('Re-deploy')
+ given(:deployment) do
+ create(:deployment, environment: environment,
+ deployable: build,
+ sha: project.commit.id)
end
- scenario 'does not show stop button' do
- expect(page).not_to have_link('Stop')
+ scenario 'does show a play button' do
+ find('.js-dropdown-play-icon-container').click
+ expect(page).to have_content(manual.name.humanize)
end
- context 'with manual action' do
- given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
+ scenario 'does allow to play manual action', js: true do
+ expect(manual).to be_skipped
- scenario 'does show a play button' do
- expect(page).to have_link(manual.name.humanize)
- end
+ find('.js-dropdown-play-icon-container').click
+ expect(page).to have_content(manual.name.humanize)
- scenario 'does allow to play manual action' do
- expect(manual).to be_skipped
- expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count }
- expect(page).to have_content(manual.name)
- expect(manual.reload).to be_pending
- end
+ expect { click_link(manual.name.humanize) }
+ .not_to change { Ci::Pipeline.count }
- context 'with external_url' do
- given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
- given(:build) { create(:ci_build, pipeline: pipeline) }
- given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+ expect(manual.reload).to be_pending
+ end
- scenario 'does show an external link button' do
- expect(page).to have_link(nil, href: environment.external_url)
- end
+ scenario 'does show build name and id' do
+ expect(page).to have_link("#{build.name} ##{build.id}")
+ end
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_selector('.stop-env-link')
+ end
+
+ scenario 'does not show external link button' do
+ expect(page).not_to have_css('external-url')
+ end
+
+ context 'with external_url' do
+ given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+
+ scenario 'does show an external link button' do
+ expect(page).to have_link(nil, href: environment.external_url)
end
+ end
- context 'with stop action' do
- given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
- given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+ context 'with stop action' do
+ given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
- scenario 'does show stop button' do
- expect(page).to have_link('Stop')
- end
+ scenario 'does show stop button' do
+ expect(page).to have_selector('.stop-env-link')
+ end
- scenario 'does allow to stop environment' do
- click_link('Stop')
+ scenario 'starts build when stop button clicked' do
+ find('.stop-env-link').click
- expect(page).to have_content('close_app')
- end
+ expect(page).to have_content('close_app')
+ end
- context 'for reporter' do
- let(:role) { :reporter }
+ context 'for reporter' do
+ let(:role) { :reporter }
- scenario 'does not show stop button' do
- expect(page).not_to have_link('Stop')
- end
+ scenario 'does not show stop button' do
+ expect(page).not_to have_selector('.stop-env-link')
end
end
end
@@ -230,9 +149,13 @@ feature 'Environments', feature: true do
end
end
+ scenario 'does have a New environment button' do
+ expect(page).to have_link('New environment')
+ end
+
describe 'when creating a new environment' do
before do
- visit namespace_project_environments_path(project.namespace, project)
+ visit_environments(project)
end
context 'when logged as developer' do
@@ -271,4 +194,8 @@ feature 'Environments', feature: true do
end
end
end
+
+ def visit_environments(project)
+ visit namespace_project_environments_path(project.namespace, project)
+ end
end
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 6c938bdead8..3934c936f20 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -182,6 +182,20 @@ feature 'Expand and collapse diffs', js: true, feature: true do
end
end
end
+
+ context 'expanding a diff when symlink was converted to a regular file' do
+ let(:branch) { 'symlink-expand-diff' }
+
+ it 'shows the content of the regular file' do
+ expect(page).to have_content('This diff is collapsed')
+ expect(page).to have_no_content('No longer a symlink')
+
+ find('.click-to-expand').click
+ wait_for_ajax
+
+ expect(page).to have_content('No longer a symlink')
+ end
+ end
end
context 'visiting a commit without collapsed diffs' do
diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb
new file mode 100644
index 00000000000..f6409e00f22
--- /dev/null
+++ b/spec/features/global_search_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+feature 'Global search', feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+
+ before do
+ project.team << [user, :master]
+ login_with(user)
+ end
+
+ describe 'I search through the issues and I see pagination' do
+ before do
+ allow_any_instance_of(Gitlab::SearchResults).to receive(:per_page).and_return(1)
+ create_list(:issue, 2, project: project, title: 'initial')
+ end
+
+ it "has a pagination" do
+ visit dashboard_projects_path
+
+ fill_in "search", with: "initial"
+ click_button "Go"
+
+ select_filter("Issues")
+ expect(page).to have_selector('.gl-pagination .page', count: 2)
+ end
+ end
+end
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
new file mode 100644
index 00000000000..476eca17a9d
--- /dev/null
+++ b/spec/features/groups/issues_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+
+feature 'Group issues page', feature: true do
+ let(:path) { issues_group_path(group) }
+ let(:issuable) { create(:issue, project: project, title: "this is my created issuable")}
+
+ include_examples 'project features apply to issuables', Issue
+end
diff --git a/spec/features/groups/labels/edit_spec.rb b/spec/features/groups/labels/edit_spec.rb
new file mode 100644
index 00000000000..69281cecb7b
--- /dev/null
+++ b/spec/features/groups/labels/edit_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+feature 'Edit group label', feature: true do
+ given(:user) { create(:user) }
+ given(:group) { create(:group) }
+ given(:label) { create(:group_label, group: group) }
+
+ background do
+ group.add_owner(user)
+ login_as(user)
+ visit edit_group_label_path(group, label)
+ end
+
+ scenario 'update label with new title' do
+ fill_in 'label_title', with: 'new label name'
+ click_button 'Save changes'
+
+ expect(current_path).to eq(root_path)
+ expect(label.reload.title).to eq('new label name')
+ end
+end
diff --git a/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb b/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb
index 33bf6d3752f..be60b0489c7 100644
--- a/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb
+++ b/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb
@@ -10,7 +10,7 @@ feature 'Groups > Members > Last owner cannot leave group', feature: true do
visit group_path(group)
end
- scenario 'user does not see a "Leave Group" link' do
- expect(page).not_to have_content 'Leave Group'
+ scenario 'user does not see a "Leave group" link' do
+ expect(page).not_to have_content 'Leave group'
end
end
diff --git a/spec/features/groups/members/member_leaves_group_spec.rb b/spec/features/groups/members/member_leaves_group_spec.rb
index 3185ff924b9..ac4d94658ae 100644
--- a/spec/features/groups/members/member_leaves_group_spec.rb
+++ b/spec/features/groups/members/member_leaves_group_spec.rb
@@ -13,7 +13,7 @@ feature 'Groups > Members > Member leaves group', feature: true do
end
scenario 'user leaves group' do
- click_link 'Leave Group'
+ click_link 'Leave group'
expect(current_path).to eq(dashboard_groups_path)
expect(group.users.exists?(user.id)).to be_falsey
diff --git a/spec/features/groups/members/owner_manages_access_requests_spec.rb b/spec/features/groups/members/owner_manages_access_requests_spec.rb
index d811b05b0c3..dbe150823ba 100644
--- a/spec/features/groups/members/owner_manages_access_requests_spec.rb
+++ b/spec/features/groups/members/owner_manages_access_requests_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
feature 'Groups > Members > Owner manages access requests', feature: true do
let(:user) { create(:user) }
let(:owner) { create(:user) }
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :access_requestable) }
background do
group.request_access(user)
diff --git a/spec/features/groups/members/sorting_spec.rb b/spec/features/groups/members/sorting_spec.rb
new file mode 100644
index 00000000000..608aedd3471
--- /dev/null
+++ b/spec/features/groups/members/sorting_spec.rb
@@ -0,0 +1,98 @@
+require 'spec_helper'
+
+feature 'Groups > Members > Sorting', feature: true do
+ let(:owner) { create(:user, name: 'John Doe') }
+ let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) }
+ let(:group) { create(:group) }
+
+ background do
+ create(:group_member, :owner, user: owner, group: group, created_at: 5.days.ago)
+ create(:group_member, :developer, user: developer, group: group, created_at: 3.days.ago)
+
+ login_as(owner)
+ end
+
+ scenario 'sorts alphabetically by default' do
+ visit_members_list(sort: nil)
+
+ expect(first_member).to include(owner.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
+ end
+
+ scenario 'sorts by access level ascending' do
+ visit_members_list(sort: :access_level_asc)
+
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(owner.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
+ end
+
+ scenario 'sorts by access level descending' do
+ visit_members_list(sort: :access_level_desc)
+
+ expect(first_member).to include(owner.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
+ end
+
+ scenario 'sorts by last joined' do
+ visit_members_list(sort: :last_joined)
+
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(owner.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
+ end
+
+ scenario 'sorts by oldest joined' do
+ visit_members_list(sort: :oldest_joined)
+
+ expect(first_member).to include(owner.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
+ end
+
+ scenario 'sorts by name ascending' do
+ visit_members_list(sort: :name_asc)
+
+ expect(first_member).to include(owner.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
+ end
+
+ scenario 'sorts by name descending' do
+ visit_members_list(sort: :name_desc)
+
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(owner.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
+ end
+
+ scenario 'sorts by recent sign in' do
+ visit_members_list(sort: :recent_sign_in)
+
+ expect(first_member).to include(owner.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
+ end
+
+ scenario 'sorts by oldest sign in' do
+ visit_members_list(sort: :oldest_sign_in)
+
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(owner.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in')
+ end
+
+ def visit_members_list(sort:)
+ visit group_group_members_path(group.to_param, sort: sort)
+ end
+
+ def first_member
+ page.all('ul.content-list > li').first.text
+ end
+
+ def second_member
+ page.all('ul.content-list > li').last.text
+ end
+end
diff --git a/spec/features/groups/members/user_requests_access_spec.rb b/spec/features/groups/members/user_requests_access_spec.rb
index b3baa2ab57c..e4b5ea91bd3 100644
--- a/spec/features/groups/members/user_requests_access_spec.rb
+++ b/spec/features/groups/members/user_requests_access_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
feature 'Groups > Members > User requests access', feature: true do
let(:user) { create(:user) }
let(:owner) { create(:user) }
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :access_requestable) }
let!(:project) { create(:project, :private, namespace: group) }
background do
@@ -29,7 +29,7 @@ feature 'Groups > Members > User requests access', feature: true do
expect(page).to have_content 'Your request for access has been queued for review.'
expect(page).to have_content 'Withdraw Access Request'
- expect(page).not_to have_content 'Leave Group'
+ expect(page).not_to have_content 'Leave group'
end
scenario 'user does not see private projects' do
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
new file mode 100644
index 00000000000..30b80aa82b0
--- /dev/null
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+feature 'Group merge requests page', feature: true do
+ let(:path) { merge_requests_group_path(group) }
+ let(:issuable) { create(:merge_request, source_project: project, target_project: project, title: 'this is my created issuable') }
+
+ include_examples 'project features apply to issuables', MergeRequest
+
+ context 'archived issuable' do
+ let(:project_archived) { create(:project, group: group, merge_requests_access_level: ProjectFeature::ENABLED, archived: true) }
+ 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 }
+
+ before do
+ issuable_archived
+ visit path
+ end
+
+ it 'hides archived merge requests' do
+ expect(page).to have_content(issuable.title)
+ expect(page).not_to have_content(issuable_archived.title)
+ end
+
+ it 'ignores archived merge request count badges in navbar' do
+ expect( page.find('[title="Merge Requests"] span.badge.count').text).to eq("1")
+ end
+
+ it 'ignores archived merge request count badges in state-filters' do
+ expect(page.find('#state-opened span.badge').text).to eq("1")
+ expect(page.find('#state-merged span.badge').text).to eq("0")
+ expect(page.find('#state-closed span.badge').text).to eq("0")
+ expect(page.find('#state-all span.badge').text).to eq("1")
+ end
+ end
+end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 13bfe90302c..4b19886274e 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -80,7 +80,7 @@ feature 'Group', feature: true do
visit path
- expect(page).to have_css('.description > p > strong')
+ expect(page).to have_css('.group-home-desc > p > strong')
end
it 'passes through html-pipeline' do
@@ -88,7 +88,7 @@ feature 'Group', feature: true do
visit path
- expect(page).to have_css('.description > p > img')
+ expect(page).to have_css('.group-home-desc > p > img')
end
it 'sanitizes unwanted tags' do
@@ -96,7 +96,7 @@ feature 'Group', feature: true do
visit path
- expect(page).not_to have_css('.description h1')
+ expect(page).not_to have_css('.group-home-desc h1')
end
it 'permits `rel` attribute on links' do
@@ -104,7 +104,7 @@ feature 'Group', feature: true do
visit path
- expect(page).to have_css('.description a[rel]')
+ expect(page).to have_css('.group-home-desc a[rel]')
end
end
end
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index e2101b333e2..40a1fced8d8 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -1,13 +1,36 @@
require 'spec_helper'
describe 'Help Pages', feature: true do
- describe 'Show SSH page' do
- before do
- login_as :user
+ describe 'Get the main help page' do
+ shared_examples_for 'help page' do |prefix: ''|
+ it 'prefixes links correctly' do
+ expect(page).to have_selector(%(div.documentation-index > ul a[href="#{prefix}/help/api/README.md"]))
+ end
end
- it 'replaces the variable $your_email with the email of the user' do
- visit help_page_path('ssh/README')
- expect(page).to have_content("ssh-keygen -t rsa -C \"#{@user.email}\"")
+
+ context 'without a trailing slash' do
+ before do
+ visit help_path
+ end
+
+ it_behaves_like 'help page'
+ end
+
+ context 'with a trailing slash' do
+ before do
+ visit help_path + '/'
+ end
+
+ it_behaves_like 'help page'
+ end
+
+ context 'with a relative installation' do
+ before do
+ stub_config_setting(relative_url_root: '/gitlab')
+ visit help_path
+ end
+
+ it_behaves_like 'help page', prefix: '/gitlab'
end
end
end
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index ef00f209998..efb53026449 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -3,72 +3,83 @@ require 'rails_helper'
describe 'Awards Emoji', feature: true do
include WaitForAjax
- let!(:project) { create(:project) }
+ let!(:project) { create(:project, :public) }
let!(:user) { create(:user) }
-
- before do
- project.team << [user, :master]
- login_as(user)
+ let(:issue) do
+ create(:issue,
+ assignee: @user,
+ project: project)
end
- describe 'Click award emoji from issue#show' do
- let!(:issue) do
- create(:issue,
- assignee: @user,
- project: project)
- end
-
- let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") }
-
+ context 'authorized user' do
before do
- visit namespace_project_issue_path(project.namespace, project, issue)
+ project.team << [user, :master]
+ login_as(user)
end
- it 'increments the thumbsdown emoji', js: true do
- find('[data-emoji="thumbsdown"]').click
- wait_for_ajax
- expect(thumbsdown_emoji).to have_text("1")
- end
+ describe 'Click award emoji from issue#show' do
+ let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") }
- context 'click the thumbsup emoji' do
- it 'increments the thumbsup emoji', js: true do
- find('[data-emoji="thumbsup"]').click
- wait_for_ajax
- expect(thumbsup_emoji).to have_text("1")
+ before do
+ visit namespace_project_issue_path(project.namespace, project, issue)
end
- it 'decrements the thumbsdown emoji', js: true do
- expect(thumbsdown_emoji).to have_text("0")
- end
- end
-
- context 'click the thumbsdown emoji' do
it 'increments the thumbsdown emoji', js: true do
find('[data-emoji="thumbsdown"]').click
wait_for_ajax
expect(thumbsdown_emoji).to have_text("1")
end
- it 'decrements the thumbsup emoji', js: true do
- expect(thumbsup_emoji).to have_text("0")
+ context 'click the thumbsup emoji' do
+ it 'increments the thumbsup emoji', js: true do
+ find('[data-emoji="thumbsup"]').click
+ wait_for_ajax
+ expect(thumbsup_emoji).to have_text("1")
+ end
+
+ it 'decrements the thumbsdown emoji', js: true do
+ expect(thumbsdown_emoji).to have_text("0")
+ end
end
- end
- it 'toggles the smiley emoji on a note', js: true do
- toggle_smiley_emoji(true)
+ context 'click the thumbsdown emoji' do
+ it 'increments the thumbsdown emoji', js: true do
+ find('[data-emoji="thumbsdown"]').click
+ wait_for_ajax
+ expect(thumbsdown_emoji).to have_text("1")
+ end
- within('.note-awards') do
- expect(find(emoji_counter)).to have_text("1")
+ it 'decrements the thumbsup emoji', js: true do
+ expect(thumbsup_emoji).to have_text("0")
+ end
end
- toggle_smiley_emoji(false)
+ it 'toggles the smiley emoji on a note', js: true do
+ toggle_smiley_emoji(true)
+
+ within('.note-awards') do
+ expect(find(emoji_counter)).to have_text("1")
+ end
+
+ toggle_smiley_emoji(false)
- within('.note-awards') do
- expect(page).not_to have_selector(emoji_counter)
+ within('.note-awards') do
+ expect(page).not_to have_selector(emoji_counter)
+ end
end
end
end
+ context 'unauthorized user', js: true do
+ before do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'has disabled emoji button' do
+ expect(first('.award-control')[:disabled]).to be(true)
+ end
+ end
+
def thumbsup_emoji
page.all(emoji_counter).first
end
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index bc2c087c9b9..832757b24d4 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -9,6 +9,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
let!(:issue2) { create(:issue, project: project, title: "Issue 2") }
let!(:bug) { create(:label, project: project, title: 'bug') }
let!(:feature) { create(:label, project: project, title: 'feature') }
+ let!(:wontfix) { create(:label, project: project, title: 'wontfix') }
context 'as an allowed user', js: true do
before do
@@ -291,6 +292,45 @@ feature 'Issues > Labels bulk assignment', feature: true do
expect(find("#issue_#{issue1.id}")).not_to have_content 'feature'
end
end
+
+ # Special case https://gitlab.com/gitlab-org/gitlab-ce/issues/24877
+ context 'unmarking common label' do
+ before do
+ issue1.labels << bug
+ issue1.labels << feature
+ issue2.labels << bug
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ it 'applies label from filtered results' do
+ check 'check_all_issues'
+
+ page.within('.issues_bulk_update') do
+ click_button 'Labels'
+ wait_for_ajax
+
+ expect(find('.dropdown-menu-labels li', text: 'bug')).to have_css('.is-active')
+ expect(find('.dropdown-menu-labels li', text: 'feature')).to have_css('.is-indeterminate')
+
+ click_link 'bug'
+ find('.dropdown-input-field', visible: true).set('wontfix')
+ click_link 'wontfix'
+ end
+
+ update_issues
+
+ page.within '.issues-holder' do
+ expect(find("#issue_#{issue1.id}")).not_to have_content 'bug'
+ expect(find("#issue_#{issue1.id}")).to have_content 'feature'
+ expect(find("#issue_#{issue1.id}")).to have_content 'wontfix'
+
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'feature'
+ expect(find("#issue_#{issue2.id}")).to have_content 'wontfix'
+ end
+ end
+ end
end
context 'as a guest' do
@@ -320,7 +360,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
def open_labels_dropdown(items = [], unmark = false)
page.within('.issues_bulk_update') do
- click_button 'Label'
+ click_button 'Labels'
wait_for_ajax
items.map do |item|
click_link item
diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
new file mode 100644
index 00000000000..762cab0c0e1
--- /dev/null
+++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
@@ -0,0 +1,76 @@
+require 'rails_helper'
+
+feature 'Resolving all open discussions in a merge request from an issue', feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: true) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let!(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request, noteable: merge_request, project: project)]).first }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ end
+
+ context 'with the internal tracker disabled' do
+ before do
+ project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'does not show a link to create a new issue' do
+ expect(page).not_to have_link 'open an issue to resolve them later'
+ end
+ end
+
+ context 'merge request has discussions that need to be resolved' do
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'shows a warning that the merge request contains unresolved discussions' do
+ expect(page).to have_content 'This merge request has unresolved discussions'
+ end
+
+ it 'has a link to resolve all discussions by creating an issue' do
+ page.within '.mr-widget-body' do
+ expect(page).to have_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_for_resolving_discussions: merge_request.iid)
+ end
+ end
+
+ context 'creating an issue for discussions' do
+ before do
+ page.click_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_for_resolving_discussions: merge_request.iid)
+ end
+
+ it 'shows an issue with the title filled in' do
+ title_field = page.find_field('issue[title]')
+
+ expect(title_field.value).to include(merge_request.title)
+ end
+
+ it 'has a mention of the discussion in the description' do
+ description_field = page.find_field('issue[description]')
+
+ expect(description_field.value).to include(discussion.first_note.note)
+ end
+
+ it 'has a hidden field for the merge request' do
+ merge_request_field = find('#merge_request_for_resolving_discussions', visible: false)
+
+ expect(merge_request_field.value).to eq(merge_request.iid.to_s)
+ end
+
+ it 'can create a new issue for the project' do
+ expect { click_button 'Submit issue' }.to change { project.issues.reload.size }.by(1)
+ end
+
+ it 'resolves the discussion in the merge request' do
+ click_button 'Submit issue'
+
+ discussion.first_note.reload
+
+ expect(discussion.resolved?).to eq(true)
+ end
+ end
+ end
+end
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb
index 2798db92f0f..0d19563d628 100644
--- a/spec/features/issues/filter_issues_spec.rb
+++ b/spec/features/issues/filter_issues_spec.rb
@@ -3,8 +3,8 @@ require 'rails_helper'
describe 'Filter issues', feature: true do
include WaitForAjax
- let!(:project) { create(:project) }
let!(:group) { create(:group) }
+ let!(:project) { create(:project, group: group) }
let!(:user) { create(:user)}
let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) }
@@ -127,7 +127,7 @@ describe 'Filter issues', feature: true do
expect(page).to have_content wontfix.title
end
- find('body').click
+ find('.dropdown-menu-close-icon').click
expect(find('.filtered-labels')).to have_content(wontfix.title)
@@ -135,7 +135,7 @@ describe 'Filter issues', feature: true do
wait_for_ajax
find('.dropdown-menu-labels a', text: label.title).click
- find('body').click
+ find('.dropdown-menu-close-icon').click
expect(find('.filtered-labels')).to have_content(wontfix.title)
expect(find('.filtered-labels')).to have_content(label.title)
@@ -150,8 +150,8 @@ describe 'Filter issues', feature: true do
it "selects and unselects `won't fix`" do
find('.dropdown-menu-labels a', text: wontfix.title).click
find('.dropdown-menu-labels a', text: wontfix.title).click
- # Close label dropdown to load
- find('body').click
+
+ find('.dropdown-menu-close-icon').click
expect(page).not_to have_css('.filtered-labels')
end
end
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
new file mode 100644
index 00000000000..da64827b377
--- /dev/null
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -0,0 +1,100 @@
+require 'rails_helper'
+
+feature 'GFM autocomplete', feature: true, js: true do
+ include WaitForAjax
+ let(:user) { create(:user, username: 'someone.special') }
+ let(:project) { create(:project) }
+ let(:label) { create(:label, project: project, title: 'special+') }
+ let(:issue) { create(:issue, project: project) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ wait_for_ajax
+ end
+
+ it 'opens autocomplete menu when field starts with text' do
+ page.within '.timeline-content-form' do
+ find('#note_note').native.send_keys('')
+ find('#note_note').native.send_keys('@')
+ end
+
+ expect(page).to have_selector('.atwho-container')
+ end
+
+ it 'doesnt open autocomplete menu character is prefixed with text' do
+ page.within '.timeline-content-form' do
+ find('#note_note').native.send_keys('testing')
+ find('#note_note').native.send_keys('@')
+ end
+
+ expect(page).not_to have_selector('.atwho-view')
+ end
+
+ context 'if a selected value has special characters' do
+ it 'wraps the result in double quotes' do
+ note = find('#note_note')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('')
+ note.native.send_keys("~#{label.title[0]}")
+ sleep 1
+ note.click
+ end
+
+ label_item = find('.atwho-view li', text: label.title)
+
+ expect_to_wrap(true, label_item, note, label.title)
+ end
+
+ it 'doesn\'t wrap for assignee values' do
+ note = find('#note_note')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('')
+ note.native.send_keys("@#{user.username[0]}")
+ sleep 1
+ note.click
+ end
+
+ user_item = find('.atwho-view li', text: user.username)
+
+ expect_to_wrap(false, user_item, note, user.username)
+ end
+
+ it 'doesn\'t wrap for emoji values' do
+ note = find('#note_note')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('')
+ note.native.send_keys(":cartwheel")
+ sleep 1
+ note.click
+ end
+
+ emoji_item = find('.atwho-view li', text: 'cartwheel_tone1')
+
+ expect_to_wrap(false, emoji_item, note, 'cartwheel_tone1')
+ end
+
+ def expect_to_wrap(should_wrap, item, note, value)
+ expect(item).to have_content(value)
+ expect(item).not_to have_content("\"#{value}\"")
+
+ item.click
+
+ if should_wrap
+ expect(note.value).to include("\"#{value}\"")
+ else
+ expect(note.value).not_to include("\"#{value}\"")
+ end
+ end
+ end
+
+ it 'doesnt open autocomplete after non-word character' do
+ page.within '.timeline-content-form' do
+ find('#note_note').native.send_keys("@#{user.username[0..2]}!")
+ end
+
+ expect(page).not_to have_selector('.atwho-view')
+ end
+end
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 4b1aec8bf71..bc068b5e7e0 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -1,7 +1,9 @@
require 'rails_helper'
feature 'Issue Sidebar', feature: true do
- let(:project) { create(:project) }
+ include WaitForAjax
+
+ let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
@@ -10,6 +12,37 @@ feature 'Issue Sidebar', feature: true do
login_as(user)
end
+ context 'assignee', js: true do
+ let(:user2) { create(:user) }
+ let(:issue2) { create(:issue, project: project, author: user2) }
+
+ before do
+ project.team << [user, :developer]
+ visit_issue(project, issue2)
+
+ find('.block.assignee .edit-link').click
+
+ wait_for_ajax
+ end
+
+ it 'shows author in assignee dropdown' do
+ page.within '.dropdown-menu-user' do
+ expect(page).to have_content(user2.name)
+ end
+ end
+
+ it 'shows author when filtering assignee dropdown' do
+ page.within '.dropdown-menu-user' do
+ find('.dropdown-input-field').native.send_keys user2.name
+ sleep 1 # Required to wait for end of input delay
+
+ wait_for_ajax
+
+ expect(page).to have_content(user2.name)
+ end
+ end
+ end
+
context 'as a allowed user' do
before do
project.team << [user, :developer]
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index 055210399a7..f89b4db9e62 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -27,8 +27,8 @@ feature 'issue move to another project' do
let!(:mr) { create(:merge_request, source_project: old_project) }
let(:new_project) { create(:project) }
let(:new_project_search) { create(:project) }
- let(:text) { 'Text with !1' }
- let(:cross_reference) { old_project.to_reference }
+ let(:text) { "Text with #{mr.to_reference}" }
+ let(:cross_reference) { old_project.to_reference(new_project) }
background do
old_project.team << [user, :reporter]
@@ -43,8 +43,8 @@ feature 'issue move to another project' do
expect(current_url).to include project_path(new_project)
- expect(page).to have_content("Text with #{cross_reference}!1")
- expect(page).to have_content("Moved from #{cross_reference}#1")
+ expect(page).to have_content("Text with #{cross_reference}#{mr.to_reference}")
+ expect(page).to have_content("moved from #{cross_reference}#{issue.to_reference}")
expect(page).to have_content(issue.title)
end
diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb
index fb0c4704285..a4d3053d10c 100644
--- a/spec/features/issues/new_branch_button_spec.rb
+++ b/spec/features/issues/new_branch_button_spec.rb
@@ -18,22 +18,24 @@ feature 'Start new branch from an issue', feature: true do
end
context "when there is a referenced merge request" do
- let(:note) do
- create(:note, :on_issue, :system, project: project,
- note: "Mentioned in !#{referenced_mr.iid}")
+ let!(:note) do
+ create(:note, :on_issue, :system, project: project, noteable: issue,
+ note: "mentioned in #{referenced_mr.to_reference}")
end
+
let(:referenced_mr) do
create(:merge_request, :simple, source_project: project, target_project: project,
- description: "Fixes ##{issue.iid}", author: user)
+ description: "Fixes #{issue.to_reference}", author: user)
end
before do
- issue.notes << note
+ referenced_mr.cache_merge_request_closes_issues!(user)
visit namespace_project_issue_path(project.namespace, project, issue)
end
it "hides the new branch button", js: true do
+ expect(page).to have_css('#new-branch .unavailable')
expect(page).not_to have_css('#new-branch .available')
expect(page).to have_content /1 Related Merge Request/
end
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
index de8fdda388d..41ff31d2b99 100644
--- a/spec/features/issues/todo_spec.rb
+++ b/spec/features/issues/todo_spec.rb
@@ -13,8 +13,8 @@ feature 'Manually create a todo item from issue', feature: true, js: true do
it 'creates todo when clicking button' do
page.within '.issuable-sidebar' do
- click_button 'Add Todo'
- expect(page).to have_content 'Mark Done'
+ click_button 'Add todo'
+ expect(page).to have_content 'Mark done'
end
page.within '.header-content .todos-pending-count' do
@@ -30,8 +30,8 @@ feature 'Manually create a todo item from issue', feature: true, js: true do
it 'marks a todo as done' do
page.within '.issuable-sidebar' do
- click_button 'Add Todo'
- click_button 'Mark Done'
+ click_button 'Add todo'
+ click_button 'Mark done'
end
expect(page).to have_selector('.todos-pending-count', visible: false)
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index cdd02a8c8e3..5c958455604 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -371,10 +371,12 @@ describe 'Issues', feature: true do
describe 'when I want to reset my incoming email token' do
let(:project1) { create(:project, namespace: @user.namespace) }
+ let(:issue) { create(:issue, project: project1) }
before do
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
project1.team << [@user, :master]
+ project1.issues << issue
visit namespace_project_issues_path(@user.namespace, project1)
end
@@ -576,7 +578,10 @@ describe 'Issues', feature: true do
describe 'new issue by email' do
shared_examples 'show the email in the modal' do
+ let(:issue) { create(:issue, project: project) }
+
before do
+ project.issues << issue
stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
visit namespace_project_issues_path(project.namespace, project)
diff --git a/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions.rb b/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb
index 7f11db3c417..7f11db3c417 100644
--- a/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions.rb
+++ b/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb
diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb
new file mode 100644
index 00000000000..dc32c8f7373
--- /dev/null
+++ b/spec/features/merge_requests/closes_issues_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+feature 'Merge Request closing issues message', feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue_1) { create(:issue, project: project)}
+ let(:issue_2) { create(:issue, project: project)}
+ let(:merge_request) do
+ create(
+ :merge_request,
+ :simple,
+ source_project: project,
+ description: merge_request_description
+ )
+ end
+ let(:merge_request_description) { 'Merge Request Description' }
+
+ before do
+ project.team << [user, :master]
+
+ login_as user
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ context 'not closing or mentioning any issue' do
+ it 'does not display closing issue message' do
+ expect(page).not_to have_css('.mr-widget-footer')
+ end
+ end
+
+ context 'closing issues but not mentioning any other issue' do
+ let(:merge_request_description) { "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" }
+
+ it 'does not display closing issue message' do
+ expect(page).to have_content("Accepting this merge request will close issues #{issue_1.to_reference} and #{issue_2.to_reference}")
+ end
+ end
+
+ context 'mentioning issues but not closing them' do
+ let(:merge_request_description) { "Description\n\nRefers to #{issue_1.to_reference} and #{issue_2.to_reference}" }
+
+ it 'does not display closing issue message' do
+ expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not closed.")
+ end
+ end
+
+ context 'closing some issues and mentioning, but not closing, others' do
+ let(:merge_request_description) { "Description\n\ncloses #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
+
+ it 'does not display closing issue message' do
+ expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not closed.")
+ end
+ end
+end
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
index d258ff52bbb..5bc4ab2dfe5 100644
--- a/spec/features/merge_requests/conflicts_spec.rb
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -20,7 +20,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do
within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do
all('button', text: 'Use ours').each do |button|
- button.click
+ button.trigger('click')
end
end
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index c68e1ea4af9..f1b68a39343 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -40,7 +40,7 @@ feature 'Create New Merge Request', feature: true, js: true do
visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_project_id: private_project.id })
- expect(page).not_to have_content private_project.to_reference
+ expect(page).not_to have_content private_project.path_with_namespace
end
end
@@ -67,4 +67,21 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).to have_content('Source branch "non-exist-source" does not exist')
expect(page).to have_content('Target branch "non-exist-target" does not exist')
end
+
+ context 'when a branch contains commits that both delete and add the same image' do
+ it 'renders the diff successfully' do
+ visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_branch: 'master', source_branch: 'deleted-image-test' })
+
+ click_link "Changes"
+
+ expect(page).to have_content "6049019_460s.jpg"
+ end
+ end
+
+ # Isolates a regression (see #24627)
+ it 'does not show error messages on initial form' do
+ visit new_namespace_project_merge_request_path(project.namespace, project)
+ expect(page).not_to have_selector('#error_explanation')
+ expect(page).not_to have_content('The form contains the following error')
+ end
end
diff --git a/spec/features/merge_requests/deleted_source_branch_spec.rb b/spec/features/merge_requests/deleted_source_branch_spec.rb
new file mode 100644
index 00000000000..d5c9ed8a3b7
--- /dev/null
+++ b/spec/features/merge_requests/deleted_source_branch_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+# This test serves as a regression test for a bug that caused an error
+# message to be shown by JavaScript when the source branch was deleted.
+# Please do not remove "js: true".
+describe 'Deleted source branch', feature: true, js: true do
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+
+ before do
+ login_as user
+ merge_request.project.team << [user, :master]
+ merge_request.update!(source_branch: 'this-branch-does-not-exist')
+ visit namespace_project_merge_request_path(
+ merge_request.project.namespace,
+ merge_request.project, merge_request
+ )
+ end
+
+ it 'shows a message about missing source branch' do
+ expect(page).to have_content(
+ 'Source branch this-branch-does-not-exist does not exist'
+ )
+ end
+
+ it 'hides Discussion, Commits and Changes tabs' do
+ within '.merge-request-details' do
+ expect(page).to have_no_content('Discussion')
+ expect(page).to have_no_content('Commits')
+ expect(page).to have_no_content('Changes')
+ end
+ end
+end
diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb
index 5e6d8467217..d5e3d8e7eff 100644
--- a/spec/features/merge_requests/diff_notes_resolve_spec.rb
+++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb
@@ -69,8 +69,6 @@ feature 'Diff notes resolve', feature: true, js: true do
page.within '.diff-content .note' do
expect(page).to have_selector('.line-resolve-btn.is-active')
-
- expect(find('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}")
end
page.within '.line-resolve-all-container' do
diff --git a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
new file mode 100644
index 00000000000..3dbe26cddb0
--- /dev/null
+++ b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+feature 'Clicking toggle commit message link', feature: true, js: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue_1) { create(:issue, project: project)}
+ let(:issue_2) { create(:issue, project: project)}
+ let(:merge_request) do
+ create(
+ :merge_request,
+ :simple,
+ source_project: project,
+ description: "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}"
+ )
+ end
+ let(:textbox) { page.find(:css, '.js-commit-message', visible: false) }
+ let(:include_link) { page.find(:css, '.js-with-description-link', visible: false) }
+ let(:do_not_include_link) { page.find(:css, '.js-without-description-link', visible: false) }
+ let(:default_message) do
+ [
+ "Merge branch 'feature' into 'master'",
+ merge_request.title,
+ "Closes #{issue_1.to_reference} and #{issue_2.to_reference}",
+ "See merge request #{merge_request.to_reference}"
+ ].join("\n\n")
+ end
+ let(:message_with_description) do
+ [
+ "Merge branch 'feature' into 'master'",
+ merge_request.title,
+ merge_request.description,
+ "See merge request #{merge_request.to_reference}"
+ ].join("\n\n")
+ end
+
+ before do
+ project.team << [user, :master]
+
+ login_as user
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ expect(textbox).not_to be_visible
+ click_link "Modify commit message"
+ expect(textbox).to be_visible
+ end
+
+ it "toggles commit message between message with description and without description " do
+ expect(textbox.value).to eq(default_message)
+
+ click_link "Include description in commit message"
+
+ expect(textbox.value).to eq(message_with_description)
+
+ click_link "Don't include description in commit message"
+
+ expect(textbox.value).to eq(default_message)
+ end
+
+ it "toggles link between 'Include description' and 'Don't include description'" do
+ expect(include_link).to be_visible
+ expect(do_not_include_link).not_to be_visible
+
+ click_link "Include description in commit message"
+
+ expect(include_link).not_to be_visible
+ expect(do_not_include_link).to be_visible
+
+ click_link "Don't include description in commit message"
+
+ expect(include_link).to be_visible
+ expect(do_not_include_link).not_to be_visible
+ end
+end
diff --git a/spec/features/merge_requests/merge_request_versions_spec.rb b/spec/features/merge_requests/merge_request_versions_spec.rb
index 23cee891bac..09451f41de4 100644
--- a/spec/features/merge_requests/merge_request_versions_spec.rb
+++ b/spec/features/merge_requests/merge_request_versions_spec.rb
@@ -3,11 +3,12 @@ require 'spec_helper'
feature 'Merge Request versions', js: true, feature: true do
let(:merge_request) { create(:merge_request, importing: true) }
let(:project) { merge_request.source_project }
+ let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
+ let!(:merge_request_diff2) { merge_request.merge_request_diffs.create(head_commit_sha: nil) }
+ let!(:merge_request_diff3) { merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
before do
login_as :admin
- merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
- merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
end
@@ -53,7 +54,7 @@ feature 'Merge Request versions', js: true, feature: true do
project.namespace,
project,
merge_request.iid,
- diff_id: 2,
+ diff_id: merge_request_diff3.id,
start_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
)
end
diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
deleted file mode 100644
index c3c3ab33872..00000000000
--- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
+++ /dev/null
@@ -1,108 +0,0 @@
-require 'spec_helper'
-
-feature 'Merge When Build Succeeds', feature: true, js: true do
- let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
-
- let(:merge_request) do
- create(:merge_request_with_diffs, source_project: project,
- author: user,
- title: 'Bug NS-04')
- end
-
- let(:pipeline) do
- create(:ci_pipeline, project: project,
- sha: merge_request.diff_head_sha,
- ref: merge_request.source_branch)
- end
-
- before { project.team << [user, :master] }
-
- context 'when there is active build for merge request' do
- background do
- create(:ci_build, pipeline: pipeline)
- end
-
- before do
- login_as user
- visit_merge_request(merge_request)
- end
-
- it 'displays the Merge When Build Succeeds button' do
- expect(page).to have_button "Merge When Build Succeeds"
- end
-
- context "Merge When Build succeeds enabled" do
- before do
- click_button "Merge When Build Succeeds"
- end
-
- it 'activates Merge When Build Succeeds feature' do
- expect(page).to have_link "Cancel Automatic Merge"
-
- expect(page).to have_content "Set by #{user.name} to be merged automatically when the build succeeds."
- expect(page).to have_content "The source branch will not be removed."
-
- visit_merge_request(merge_request) # Needed to refresh the page
- expect(page).to have_content /Enabled an automatic merge when the build for [0-9a-f]{8} succeeds/i
- end
- end
- end
-
- context 'when merge when build succeeds is enabled' do
- let(:merge_request) do
- create(:merge_request_with_diffs, :simple, source_project: project,
- author: user,
- merge_user: user,
- title: 'MepMep',
- merge_when_build_succeeds: true)
- end
-
- let!(:build) do
- create(:ci_build, pipeline: pipeline)
- end
-
- before do
- login_as user
- visit_merge_request(merge_request)
- end
-
- it 'allows to cancel the automatic merge' do
- click_link "Cancel Automatic Merge"
-
- expect(page).to have_button "Merge When Build Succeeds"
-
- visit_merge_request(merge_request) # refresh the page
- expect(page).to have_content "Canceled the automatic merge"
- end
-
- it "allows the user to remove the source branch" do
- expect(page).to have_link "Remove Source Branch When Merged"
-
- click_link "Remove Source Branch When Merged"
- expect(page).to have_content "The source branch will be removed"
- end
-
- context 'when build succeeds' do
- background { build.success }
-
- it 'merges merge request' do
- visit_merge_request(merge_request) # refresh the page
-
- expect(page).to have_content 'The changes were merged'
- expect(merge_request.reload).to be_merged
- end
- end
- end
-
- context 'when build is not active' do
- it "does not allow to enable merge when build succeeds" do
- visit_merge_request(merge_request)
- expect(page).not_to have_link "Merge When Build Succeeds"
- end
- end
-
- def visit_merge_request(merge_request)
- visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
- end
-end
diff --git a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
new file mode 100644
index 00000000000..aa24a905001
--- /dev/null
+++ b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
@@ -0,0 +1,109 @@
+require 'spec_helper'
+
+feature 'Merge When Pipeline Succeeds', :feature, :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ let(:merge_request) do
+ create(:merge_request_with_diffs, source_project: project,
+ author: user,
+ title: 'Bug NS-04')
+ end
+
+ let(:pipeline) do
+ create(:ci_pipeline, project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch)
+ end
+
+ before { project.team << [user, :master] }
+
+ context 'when there is active pipeline for merge request' do
+ background do
+ create(:ci_build, pipeline: pipeline)
+ end
+
+ before do
+ login_as user
+ visit_merge_request(merge_request)
+ end
+
+ it 'displays the Merge When Pipeline Succeeds button' do
+ expect(page).to have_button "Merge When Pipeline Succeeds"
+ end
+
+ context "Merge When Pipeline Succeeds enabled" do
+ before do
+ click_button "Merge When Pipeline Succeeds"
+ end
+
+ it 'activates Merge When Pipeline Succeeds feature' do
+ expect(page).to have_link "Cancel Automatic Merge"
+
+ expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds."
+ expect(page).to have_content "The source branch will not be removed."
+
+ visit_merge_request(merge_request) # Needed to refresh the page
+ expect(page).to have_content /enabled an automatic merge when the pipeline for \h{8} succeeds/i
+ end
+ end
+ end
+
+ context 'when merge when pipeline succeeds is enabled' do
+ let(:merge_request) do
+ create(:merge_request_with_diffs, :simple, source_project: project,
+ author: user,
+ merge_user: user,
+ title: 'MepMep',
+ merge_when_build_succeeds: true)
+ end
+
+ let!(:build) do
+ create(:ci_build, pipeline: pipeline)
+ end
+
+ before do
+ login_as user
+ visit_merge_request(merge_request)
+ end
+
+ it 'allows to cancel the automatic merge' do
+ click_link "Cancel Automatic Merge"
+
+ expect(page).to have_button "Merge When Pipeline Succeeds"
+
+ visit_merge_request(merge_request) # refresh the page
+ expect(page).to have_content "canceled the automatic merge"
+ end
+
+ it "allows the user to remove the source branch" do
+ expect(page).to have_link "Remove Source Branch When Merged"
+
+ click_link "Remove Source Branch When Merged"
+ expect(page).to have_content "The source branch will be removed"
+ end
+
+ context 'when pipeline succeeds' do
+ background { build.success }
+
+ it 'merges merge request' do
+ visit_merge_request(merge_request) # refresh the page
+
+ expect(page).to have_content 'The changes were merged'
+ expect(merge_request.reload).to be_merged
+ end
+ end
+ end
+
+ context 'when pipeline is not active' do
+ it "does not allow to enable merge when pipeline succeeds" do
+ visit_merge_request(merge_request)
+
+ expect(page).not_to have_link 'Merge When Pipeline Succeeds'
+ end
+ end
+
+ def visit_merge_request(merge_request)
+ visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
+ end
+end
diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb
deleted file mode 100644
index 80e8b8fc642..00000000000
--- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb
+++ /dev/null
@@ -1,105 +0,0 @@
-require 'spec_helper'
-
-feature 'Only allow merge requests to be merged if the build succeeds', feature: true do
- let(:project) { create(:project, :public) }
- let(:merge_request) { create(:merge_request_with_diffs, source_project: project) }
-
- before do
- login_as merge_request.author
-
- project.team << [merge_request.author, :master]
- end
-
- context 'project does not have CI enabled' do
- it 'allows MR to be merged' do
- visit_merge_request(merge_request)
-
- expect(page).to have_button 'Accept Merge Request'
- end
- end
-
- context 'when project has CI enabled' do
- let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) }
-
- context 'when merge requests can only be merged if the build succeeds' do
- before do
- project.update_attribute(:only_allow_merge_if_build_succeeds, true)
- end
-
- context 'when CI is running' do
- before { pipeline.update_column(:status, :running) }
-
- it 'does not allow to merge immediately' do
- visit_merge_request(merge_request)
-
- expect(page).to have_button 'Merge When Build Succeeds'
- expect(page).not_to have_button 'Select Merge Moment'
- end
- end
-
- context 'when CI failed' do
- before { pipeline.update_column(:status, :failed) }
-
- it 'does not allow MR to be merged' do
- 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.')
- end
- end
-
- context 'when CI succeeded' do
- before { pipeline.update_column(:status, :success) }
-
- it 'allows MR to be merged' do
- visit_merge_request(merge_request)
-
- expect(page).to have_button 'Accept Merge Request'
- end
- end
- end
-
- context 'when merge requests can be merged when the build failed' do
- before do
- project.update_attribute(:only_allow_merge_if_build_succeeds, false)
- end
-
- context 'when CI is running' do
- before { pipeline.update_column(:status, :running) }
-
- it 'allows MR to be merged immediately', js: true do
- visit_merge_request(merge_request)
-
- expect(page).to have_button 'Merge When Build Succeeds'
-
- click_button 'Select Merge Moment'
- expect(page).to have_content 'Merge Immediately'
- end
- end
-
- context 'when CI failed' do
- before { pipeline.update_column(:status, :failed) }
-
- it 'allows MR to be merged' do
- visit_merge_request(merge_request)
-
- expect(page).to have_button 'Accept Merge Request'
- end
- end
-
- context 'when CI succeeded' do
- before { pipeline.update_column(:status, :success) }
-
- it 'allows MR to be merged' do
- visit_merge_request(merge_request)
-
- expect(page).to have_button 'Accept Merge Request'
- end
- end
- end
- end
-
- def visit_merge_request(merge_request)
- visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
- 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
new file mode 100644
index 00000000000..7e2907cd26f
--- /dev/null
+++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
@@ -0,0 +1,132 @@
+require 'spec_helper'
+
+feature 'Only allow merge requests to be merged if the build succeeds', feature: true do
+ let(:merge_request) { create(:merge_request_with_diffs) }
+ let(:project) { merge_request.target_project }
+
+ before do
+ login_as merge_request.author
+
+ project.team << [merge_request.author, :master]
+ end
+
+ context 'project does not have CI enabled' do
+ it 'allows MR to be merged' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Accept Merge Request'
+ end
+ end
+
+ context 'when project has CI enabled' do
+ given!(:pipeline) do
+ create(:ci_empty_pipeline,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ status: status)
+ end
+
+ context 'when merge requests can only be merged if the build succeeds' do
+ before do
+ project.update_attribute(:only_allow_merge_if_build_succeeds, true)
+ end
+
+ context 'when CI is running' do
+ given(:status) { :running }
+
+ it 'does not allow to merge immediately' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Merge When Pipeline Succeeds'
+ expect(page).not_to have_button 'Select Merge Moment'
+ end
+ end
+
+ context 'when CI failed' do
+ given(:status) { :failed }
+
+ it 'does not allow MR to be merged' do
+ 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.')
+ end
+ end
+
+ context 'when CI canceled' do
+ given(:status) { :canceled }
+
+ it 'does not allow MR to be merged' do
+ 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.')
+ end
+ end
+
+ context 'when CI succeeded' do
+ given(:status) { :success }
+
+ it 'allows MR to be merged' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Accept Merge Request'
+ end
+ end
+
+ context 'when CI skipped' do
+ given(:status) { :skipped }
+
+ it 'allows MR to be merged' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Accept Merge Request'
+ end
+ end
+ end
+
+ context 'when merge requests can be merged when the build failed' do
+ before do
+ project.update_attribute(:only_allow_merge_if_build_succeeds, false)
+ end
+
+ context 'when CI is running' do
+ given(:status) { :running }
+
+ it 'allows MR to be merged immediately', js: true do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Merge When Pipeline Succeeds'
+
+ click_button 'Select Merge Moment'
+ expect(page).to have_content 'Merge Immediately'
+ end
+ end
+
+ context 'when CI failed' do
+ given(:status) { :failed }
+
+ it 'allows MR to be merged' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Accept Merge Request'
+ end
+ end
+
+ context 'when CI succeeded' do
+ given(:status) { :success }
+
+ it 'allows MR to be merged' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Accept Merge Request'
+ end
+ end
+ end
+ end
+
+ def visit_merge_request(merge_request)
+ visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
+ end
+end
diff --git a/spec/features/merge_requests/target_branch_spec.rb b/spec/features/merge_requests/target_branch_spec.rb
new file mode 100644
index 00000000000..b6134540273
--- /dev/null
+++ b/spec/features/merge_requests/target_branch_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe 'Target branch', feature: true do
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.project }
+
+ def path_to_merge_request
+ namespace_project_merge_request_path(
+ project.namespace,
+ project, merge_request
+ )
+ end
+
+ before do
+ login_as user
+ project.team << [user, :master]
+ end
+
+ it 'shows link to target branch' do
+ visit path_to_merge_request
+ expect(page).to have_link('feature', href: namespace_project_commits_path(project.namespace, project, merge_request.target_branch))
+ end
+
+ context 'when branch was deleted' do
+ before do
+ DeleteBranchService.new(project, user).execute('feature')
+ visit path_to_merge_request
+ end
+
+ it 'shows a message about missing target branch' do
+ expect(page).to have_content(
+ 'Target branch feature does not exist'
+ )
+ end
+
+ it 'does not show link to target branch' do
+ expect(page).not_to have_link('feature')
+ end
+ end
+end
diff --git a/spec/features/merge_requests/toggle_whitespace_changes.rb b/spec/features/merge_requests/toggle_whitespace_changes_spec.rb
index 0f98737b700..0f98737b700 100644
--- a/spec/features/merge_requests/toggle_whitespace_changes.rb
+++ b/spec/features/merge_requests/toggle_whitespace_changes_spec.rb
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index b8c838bf7ab..a2e40546588 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -14,12 +14,17 @@ feature 'Milestone', feature: true do
feature 'Create a milestone' do
scenario 'shows an informative message for a new milestone' do
visit new_namespace_project_milestone_path(project.namespace, project)
+
page.within '.milestone-form' do
fill_in "milestone_title", with: '8.7'
+ fill_in "milestone_start_date", with: '2016-11-16'
+ fill_in "milestone_due_date", with: '2016-12-16'
end
+
find('input[name="commit"]').click
expect(find('.alert-success')).to have_content('Assign some issues to this milestone.')
+ expect(page).to have_content('Nov 16, 2016 - Dec 16, 2016')
end
end
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index 5d7247e2a62..9fffbb43e87 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -141,7 +141,7 @@ describe 'Comments', feature: true do
let(:project2) { create(:project, :private) }
let(:issue) { create(:issue, project: project2) }
let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'markdown') }
- let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: project, note: "Mentioned in #{issue.to_reference(project)}") }
+ let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: project, note: "mentioned in #{issue.to_reference(project)}") }
it 'shows the system note' do
login_as :admin
diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb
index a78a1c9c890..c2545b0c259 100644
--- a/spec/features/participants_autocomplete_spec.rb
+++ b/spec/features/participants_autocomplete_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
feature 'Member autocomplete', feature: true do
+ include WaitForAjax
+
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
let(:participant) { create(:user) }
@@ -79,11 +81,10 @@ feature 'Member autocomplete', feature: true do
end
def open_member_suggestions
- sleep 1
page.within('.new-note') do
- sleep 1
- find('#note_note').native.send_keys('@')
+ find('#note_note').send_keys('@')
end
+ wait_for_ajax
end
def visit_issue(project, issue)
diff --git a/spec/features/profiles/chat_names_spec.rb b/spec/features/profiles/chat_names_spec.rb
new file mode 100644
index 00000000000..6f6f7029c0b
--- /dev/null
+++ b/spec/features/profiles/chat_names_spec.rb
@@ -0,0 +1,77 @@
+require 'rails_helper'
+
+feature 'Profile > Chat', feature: true do
+ given(:user) { create(:user) }
+ given(:service) { create(:service) }
+
+ before do
+ login_as(user)
+ end
+
+ describe 'uses authorization link' do
+ given(:params) do
+ { team_id: 'T00', team_domain: 'my_chat_team', user_id: 'U01', user_name: 'my_chat_user' }
+ end
+ given!(:authorize_url) { ChatNames::AuthorizeUserService.new(service, params).execute }
+ given(:authorize_path) { URI.parse(authorize_url).request_uri }
+
+ before do
+ visit authorize_path
+ end
+
+ context 'clicks authorize' do
+ before do
+ click_button 'Authorize'
+ end
+
+ scenario 'goes to list of chat names and see chat account' do
+ expect(page.current_path).to eq(profile_chat_names_path)
+ expect(page).to have_content('my_chat_team')
+ expect(page).to have_content('my_chat_user')
+ end
+
+ scenario 'second use of link is denied' do
+ visit authorize_path
+
+ expect(page).to have_http_status(:not_found)
+ end
+ end
+
+ context 'clicks deny' do
+ before do
+ click_button 'Deny'
+ end
+
+ scenario 'goes to list of chat names and do not see chat account' do
+ expect(page.current_path).to eq(profile_chat_names_path)
+ expect(page).not_to have_content('my_chat_team')
+ expect(page).not_to have_content('my_chat_user')
+ end
+
+ scenario 'second use of link is denied' do
+ visit authorize_path
+
+ expect(page).to have_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'visits chat accounts' do
+ given!(:chat_name) { create(:chat_name, user: user, service: service) }
+
+ before do
+ visit profile_chat_names_path
+ end
+
+ scenario 'sees chat user' do
+ expect(page).to have_content(chat_name.team_domain)
+ expect(page).to have_content(chat_name.chat_name)
+ end
+
+ scenario 'removes chat account' do
+ click_link 'Remove'
+
+ expect(page).to have_content("You don't have any active chat names.")
+ end
+ end
+end
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index a85930c7543..55a01057c83 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -27,28 +27,25 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do
describe "token creation" do
it "allows creation of a token" do
- visit profile_personal_access_tokens_path
- fill_in "Name", with: FFaker::Product.brand
-
- expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1)
- expect(created_personal_access_token).to eq(PersonalAccessToken.last.token)
- expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name)
- expect(active_personal_access_tokens).to have_text("Never")
- end
+ name = FFaker::Product.brand
- it "allows creation of a token with an expiry date" do
visit profile_personal_access_tokens_path
- fill_in "Name", with: FFaker::Product.brand
+ fill_in "Name", with: name
# Set date to 1st of next month
find_field("Expires at").trigger('focus')
find("a[title='Next']").click
click_on "1"
- expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1)
- expect(created_personal_access_token).to eq(PersonalAccessToken.last.token)
- expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name)
+ # Scopes
+ check "api"
+ check "read_user"
+
+ click_on "Create Personal Access Token"
+ expect(active_personal_access_tokens).to have_text(name)
expect(active_personal_access_tokens).to have_text(Date.today.next_month.at_beginning_of_month.to_s(:medium))
+ expect(active_personal_access_tokens).to have_text('api')
+ expect(active_personal_access_tokens).to have_text('read_user')
end
context "when creation fails" do
@@ -85,7 +82,7 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do
disallow_personal_access_token_saves!
visit profile_personal_access_tokens_path
- expect { click_on "Revoke" }.not_to change { PersonalAccessToken.inactive.count }
+ click_on "Revoke"
expect(active_personal_access_tokens).to have_text(personal_access_token.name)
expect(page).to have_content("Could not revoke")
end
diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb
index d14a1158b67..a6b841c0210 100644
--- a/spec/features/profiles/preferences_spec.rb
+++ b/spec/features/profiles/preferences_spec.rb
@@ -73,7 +73,7 @@ describe 'Profile > Preferences', feature: true do
expect(page.current_path).to eq starred_dashboard_projects_path
end
- click_link 'Your Projects'
+ click_link 'Your projects'
expect(page).not_to have_content("You don't have starred projects yet")
expect(page.current_path).to eq dashboard_projects_path
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
new file mode 100644
index 00000000000..a820d07ab3b
--- /dev/null
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+feature 'Editing file blob', feature: true, js: true do
+ include WaitForAjax
+
+ given(:user) { create(:user) }
+ given(:role) { :developer }
+ given(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') }
+ given(:project) { merge_request.target_project }
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ end
+
+ def edit_and_commit
+ wait_for_ajax
+ first('.file-actions').click_link 'Edit'
+ execute_script('ace.edit("editor").setValue("class NextFeature\nend\n")')
+ click_button 'Commit Changes'
+ end
+
+ context 'from MR diff' do
+ before do
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
+ edit_and_commit
+ end
+
+ scenario 'returns me to the mr' do
+ expect(page).to have_content(merge_request.title)
+ end
+ end
+
+ context 'from blob file path' do
+ before do
+ visit namespace_project_blob_path(project.namespace, project, '/feature/files/ruby/feature.rb')
+ edit_and_commit
+ end
+
+ scenario 'updates content' do
+ expect(page).to have_content 'successfully committed'
+ expect(page).to have_content 'NextFeature'
+ end
+ end
+end
diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb
index a8022a5361f..8c4d4320dc5 100644
--- a/spec/features/projects/builds_spec.rb
+++ b/spec/features/projects/builds_spec.rb
@@ -1,52 +1,59 @@
require 'spec_helper'
require 'tempfile'
-describe "Builds" do
- let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
+feature 'Builds', :feature do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
+ let(:build2) { create(:ci_build) }
+
+ let(:artifacts_file) do
+ fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')
+ end
before do
- login_as(:user)
- @commit = FactoryGirl.create :ci_pipeline
- @build = FactoryGirl.create :ci_build, :trace, pipeline: @commit
- @build2 = FactoryGirl.create :ci_build
- @project = @commit.project
- @project.team << [@user, :developer]
+ project.team << [user, :developer]
+ login_as(user)
end
describe "GET /:project/builds" do
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
+
context "Pending scope" do
before do
- visit namespace_project_builds_path(@project.namespace, @project, scope: :pending)
+ visit namespace_project_builds_path(project.namespace, project, scope: :pending)
end
it "shows Pending tab builds" 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
- expect(page).to have_content @build.ref
- expect(page).to have_content @build.name
+ expect(page).to have_content build.short_sha
+ expect(page).to have_content build.ref
+ expect(page).to have_content build.name
end
end
context "Running scope" do
before do
- @build.run!
- visit namespace_project_builds_path(@project.namespace, @project, scope: :running)
+ build.run!
+ visit namespace_project_builds_path(project.namespace, project, scope: :running)
end
it "shows Running tab builds" 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
- expect(page).to have_content @build.ref
- expect(page).to have_content @build.name
+ expect(page).to have_content build.short_sha
+ expect(page).to have_content build.ref
+ expect(page).to have_content build.name
end
end
context "Finished scope" do
before do
- @build.run!
- visit namespace_project_builds_path(@project.namespace, @project, scope: :finished)
+ build.run!
+ visit namespace_project_builds_path(project.namespace, project, scope: :finished)
end
it "shows Finished tab builds" do
@@ -58,15 +65,15 @@ describe "Builds" do
context "All builds" do
before do
- @project.builds.running_or_pending.each(&:success)
- visit namespace_project_builds_path(@project.namespace, @project)
+ project.builds.running_or_pending.each(&:success)
+ visit namespace_project_builds_path(project.namespace, project)
end
it "shows All tab builds" 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
- expect(page).to have_content @build.name
+ expect(page).to have_content build.short_sha
+ expect(page).to have_content build.ref
+ expect(page).to have_content build.name
expect(page).not_to have_link 'Cancel running'
end
end
@@ -74,17 +81,17 @@ describe "Builds" do
describe "POST /:project/builds/:id/cancel_all" do
before do
- @build.run!
- visit namespace_project_builds_path(@project.namespace, @project)
+ build.run!
+ visit namespace_project_builds_path(project.namespace, project)
click_link "Cancel running"
end
it 'shows all necessary content' do
expect(page).to have_selector('.nav-links li.active', text: 'All')
expect(page).to have_content 'canceled'
- expect(page).to have_content @build.short_sha
- expect(page).to have_content @build.ref
- expect(page).to have_content @build.name
+ expect(page).to have_content build.short_sha
+ expect(page).to have_content build.ref
+ expect(page).to have_content build.name
expect(page).not_to have_link 'Cancel running'
end
end
@@ -92,20 +99,20 @@ describe "Builds" do
describe "GET /:project/builds/:id" do
context "Build from project" do
before do
- visit namespace_project_build_path(@project.namespace, @project, @build)
+ visit namespace_project_build_path(project.namespace, project, build)
end
it 'shows commit`s data' do
expect(page.status_code).to eq(200)
- expect(page).to have_content @commit.sha[0..7]
- expect(page).to have_content @commit.git_commit_message
- expect(page).to have_content @commit.git_author_name
+ expect(page).to have_content pipeline.sha[0..7]
+ expect(page).to have_content pipeline.git_commit_message
+ expect(page).to have_content pipeline.git_author_name
end
end
context "Build from other project" do
before do
- visit namespace_project_build_path(@project.namespace, @project, @build2)
+ visit namespace_project_build_path(project.namespace, project, build2)
end
it { expect(page.status_code).to eq(404) }
@@ -113,8 +120,8 @@ describe "Builds" do
context "Download artifacts" do
before do
- @build.update_attributes(artifacts_file: artifacts_file)
- visit namespace_project_build_path(@project.namespace, @project, @build)
+ build.update_attributes(artifacts_file: artifacts_file)
+ visit namespace_project_build_path(project.namespace, project, build)
end
it 'has button to download artifacts' do
@@ -124,8 +131,8 @@ describe "Builds" do
context 'Artifacts expire date' do
before do
- @build.update_attributes(artifacts_file: artifacts_file, artifacts_expire_at: expire_at)
- visit namespace_project_build_path(@project.namespace, @project, @build)
+ build.update_attributes(artifacts_file: artifacts_file, artifacts_expire_at: expire_at)
+ visit namespace_project_build_path(project.namespace, project, build)
end
context 'no expire date defined' do
@@ -158,10 +165,10 @@ describe "Builds" do
end
end
- context 'Build raw trace' do
+ feature 'Raw trace' do
before do
- @build.run!
- visit namespace_project_build_path(@project.namespace, @project, @build)
+ build.run!
+ visit namespace_project_build_path(project.namespace, project, build)
end
it do
@@ -169,11 +176,43 @@ describe "Builds" do
end
end
- describe 'Variables' do
+ feature 'HTML trace', :js do
before do
- @trigger_request = create :ci_trigger_request_with_variables
- @build = create :ci_build, pipeline: @commit, trigger_request: @trigger_request
- visit namespace_project_build_path(@project.namespace, @project, @build)
+ build.run!
+
+ visit namespace_project_build_path(project.namespace, project, build)
+ end
+
+ context 'when build has an initial trace' do
+ it 'loads build trace' do
+ expect(page).to have_content 'BUILD TRACE'
+
+ build.append_trace(' and more trace', 11)
+
+ expect(page).to have_content 'BUILD TRACE and more trace'
+ end
+ end
+
+ context 'when build does not have an initial trace' do
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ it 'loads new trace' do
+ build.append_trace('build trace', 0)
+
+ expect(page).to have_content 'build trace'
+ end
+ end
+ end
+
+ feature 'Variables' do
+ let(:trigger_request) { create(:ci_trigger_request_with_variables) }
+
+ let(:build) do
+ create :ci_build, pipeline: pipeline, trigger_request: trigger_request
+ end
+
+ before do
+ visit namespace_project_build_path(project.namespace, project, build)
end
it 'shows variable key and value after click', js: true do
@@ -188,13 +227,50 @@ describe "Builds" do
expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1')
end
end
+
+ context 'when build starts environment' do
+ let(:environment) { create(:environment, project: project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ context 'build 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
+ 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
+ let(:build) { create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) }
+
+ it 'shows a link for the build' 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
+ let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) }
+ let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) }
+
+ it 'shows a link to lastest deployment' do
+ visit namespace_project_build_path(project.namespace, project, build)
+
+ expect(page).to have_link('latest deployment')
+ end
+ end
+ end
end
describe "POST /:project/builds/:id/cancel" do
context "Build from project" do
before do
- @build.run!
- visit namespace_project_build_path(@project.namespace, @project, @build)
+ build.run!
+ visit namespace_project_build_path(project.namespace, project, build)
click_link "Cancel"
end
@@ -207,9 +283,9 @@ describe "Builds" do
context "Build from other project" do
before do
- @build.run!
- visit namespace_project_build_path(@project.namespace, @project, @build)
- page.driver.post(cancel_namespace_project_build_path(@project.namespace, @project, @build2))
+ build.run!
+ visit namespace_project_build_path(project.namespace, project, build)
+ page.driver.post(cancel_namespace_project_build_path(project.namespace, project, build2))
end
it { expect(page.status_code).to eq(404) }
@@ -219,8 +295,8 @@ describe "Builds" do
describe "POST /:project/builds/:id/retry" do
context "Build from project" do
before do
- @build.run!
- visit namespace_project_build_path(@project.namespace, @project, @build)
+ build.run!
+ visit namespace_project_build_path(project.namespace, project, build)
click_link 'Cancel'
page.within('.build-header') do
click_link 'Retry build'
@@ -238,10 +314,10 @@ describe "Builds" do
context "Build from other project" do
before do
- @build.run!
- visit namespace_project_build_path(@project.namespace, @project, @build)
+ build.run!
+ visit namespace_project_build_path(project.namespace, project, build)
click_link 'Cancel'
- page.driver.post(retry_namespace_project_build_path(@project.namespace, @project, @build2))
+ page.driver.post(retry_namespace_project_build_path(project.namespace, project, build2))
end
it { expect(page).to have_http_status(404) }
@@ -249,13 +325,13 @@ describe "Builds" do
context "Build that current user is not allowed to retry" do
before do
- @build.run!
- @build.cancel!
- @project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ build.run!
+ build.cancel!
+ project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
logout_direct
login_with(create(:user))
- visit namespace_project_build_path(@project.namespace, @project, @build)
+ visit namespace_project_build_path(project.namespace, project, build)
end
it 'does not show the Retry button' do
@@ -268,15 +344,15 @@ describe "Builds" do
describe "GET /:project/builds/:id/download" do
before do
- @build.update_attributes(artifacts_file: artifacts_file)
- visit namespace_project_build_path(@project.namespace, @project, @build)
+ build.update_attributes(artifacts_file: artifacts_file)
+ visit namespace_project_build_path(project.namespace, project, build)
click_link 'Download'
end
context "Build from other project" do
before do
- @build2.update_attributes(artifacts_file: artifacts_file)
- visit download_namespace_project_build_artifacts_path(@project.namespace, @project, @build2)
+ build2.update_attributes(artifacts_file: artifacts_file)
+ visit download_namespace_project_build_artifacts_path(project.namespace, project, build2)
end
it { expect(page.status_code).to eq(404) }
@@ -288,23 +364,23 @@ describe "Builds" do
context 'build from project' do
before do
Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
- @build.run!
- visit namespace_project_build_path(@project.namespace, @project, @build)
+ build.run!
+ visit namespace_project_build_path(project.namespace, project, build)
page.within('.js-build-sidebar') { click_link 'Raw' }
end
it 'sends the right headers' do
expect(page.status_code).to eq(200)
expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace)
+ expect(page.response_headers['X-Sendfile']).to eq(build.path_to_trace)
end
end
context 'build from other project' do
before do
Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
- @build2.run!
- visit raw_namespace_project_build_path(@project.namespace, @project, @build2)
+ build2.run!
+ visit raw_namespace_project_build_path(project.namespace, project, build2)
end
it 'sends the right headers' do
@@ -325,8 +401,8 @@ describe "Builds" do
context 'when build has trace in file' do
before do
Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
- @build.run!
- visit namespace_project_build_path(@project.namespace, @project, @build)
+ build.run!
+ visit namespace_project_build_path(project.namespace, project, build)
allow_any_instance_of(Project).to receive(:ci_id).and_return(nil)
allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(existing_file)
@@ -345,8 +421,8 @@ describe "Builds" do
context 'when build has trace in old file' do
before do
Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
- @build.run!
- visit namespace_project_build_path(@project.namespace, @project, @build)
+ build.run!
+ visit namespace_project_build_path(project.namespace, project, build)
allow_any_instance_of(Project).to receive(:ci_id).and_return(999)
allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(non_existing_file)
@@ -365,8 +441,8 @@ describe "Builds" do
context 'when build has trace in DB' do
before do
Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
- @build.run!
- visit namespace_project_build_path(@project.namespace, @project, @build)
+ build.run!
+ visit namespace_project_build_path(project.namespace, project, build)
allow_any_instance_of(Project).to receive(:ci_id).and_return(nil)
allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(non_existing_file)
@@ -385,7 +461,7 @@ describe "Builds" do
describe "GET /:project/builds/:id/trace.json" do
context "Build from project" do
before do
- visit trace_namespace_project_build_path(@project.namespace, @project, @build, format: :json)
+ visit trace_namespace_project_build_path(project.namespace, project, build, format: :json)
end
it { expect(page.status_code).to eq(200) }
@@ -393,7 +469,7 @@ describe "Builds" do
context "Build from other project" do
before do
- visit trace_namespace_project_build_path(@project.namespace, @project, @build2, format: :json)
+ visit trace_namespace_project_build_path(project.namespace, project, build2, format: :json)
end
it { expect(page.status_code).to eq(404) }
@@ -403,7 +479,7 @@ describe "Builds" do
describe "GET /:project/builds/:id/status" do
context "Build from project" do
before do
- visit status_namespace_project_build_path(@project.namespace, @project, @build)
+ visit status_namespace_project_build_path(project.namespace, project, build)
end
it { expect(page.status_code).to eq(200) }
@@ -411,7 +487,7 @@ describe "Builds" do
context "Build from other project" do
before do
- visit status_namespace_project_build_path(@project.namespace, @project, @build2)
+ visit status_namespace_project_build_path(project.namespace, project, build2)
end
it { expect(page.status_code).to eq(404) }
diff --git a/spec/features/projects/commits/cherry_pick_spec.rb b/spec/features/projects/commits/cherry_pick_spec.rb
index e45e3a36d01..d46d9e9399e 100644
--- a/spec/features/projects/commits/cherry_pick_spec.rb
+++ b/spec/features/projects/commits/cherry_pick_spec.rb
@@ -64,7 +64,7 @@ describe 'Cherry-pick Commits' do
context "I cherry-pick a commit from a different branch", js: true do
it do
- find('.commit-action-buttons a.dropdown-toggle').click
+ find('.header-action-buttons a.dropdown-toggle').click
find(:css, "a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 09aa6758b5c..3bb33394be7 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -182,6 +182,44 @@ describe 'Edit Project Settings', feature: true do
expect(page).not_to have_content("Comments")
end
end
+
+ # Regression spec for https://gitlab.com/gitlab-org/gitlab-ce/issues/25272
+ it "hides comments activity tab only on disabled issues, merge requests and repository" do
+ select "Disabled", from: "project_project_feature_attributes_issues_access_level"
+
+ save_changes_and_check_activity_tab do
+ expect(page).to have_content("Comments")
+ end
+
+ visit edit_namespace_project_path(project.namespace, project)
+
+ select "Disabled", from: "project_project_feature_attributes_merge_requests_access_level"
+
+ save_changes_and_check_activity_tab do
+ expect(page).to have_content("Comments")
+ end
+
+ visit edit_namespace_project_path(project.namespace, project)
+
+ select "Disabled", from: "project_project_feature_attributes_repository_access_level"
+
+ save_changes_and_check_activity_tab do
+ expect(page).not_to have_content("Comments")
+ end
+
+ visit edit_namespace_project_path(project.namespace, project)
+ end
+
+ def save_changes_and_check_activity_tab
+ click_button "Save changes"
+ wait_for_ajax
+
+ visit activity_namespace_project_path(project.namespace, project)
+
+ page.within(".event-filter") do
+ yield
+ end
+ end
end
# Regression spec for https://gitlab.com/gitlab-org/gitlab-ce/issues/24056
diff --git a/spec/features/projects/files/creating_a_file_spec.rb b/spec/features/projects/files/creating_a_file_spec.rb
new file mode 100644
index 00000000000..ae448706130
--- /dev/null
+++ b/spec/features/projects/files/creating_a_file_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+feature 'User wants to create a file', feature: true do
+ include WaitForAjax
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ background do
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_new_blob_path(project.namespace, project, project.default_branch)
+ end
+
+ def submit_new_file(options)
+ file_name = find('#file_name')
+ file_name.set options[:file_name] || 'README.md'
+
+ file_content = find('#file-content')
+ file_content.set options[:file_content] || 'Some content'
+
+ click_button 'Commit Changes'
+ end
+
+ scenario 'file name contains Chinese characters' do
+ submit_new_file(file_name: '测试.md')
+ expect(page).to have_content 'The file has been successfully created.'
+ end
+
+ scenario 'directory name contains Chinese characters' do
+ submit_new_file(file_name: '中文/测试.md')
+ expect(page).to have_content 'The file has been successfully created.'
+ end
+
+ scenario 'file name contains invalid characters' do
+ submit_new_file(file_name: '\\')
+ expect(page).to have_content 'Your changes could not be committed, because the file name can contain only'
+ end
+
+ scenario 'file name contains directory traversal' do
+ submit_new_file(file_name: '../README.md')
+ expect(page).to have_content 'Your changes could not be committed, because the file name cannot include directory traversal.'
+ end
+end
diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb
index 1921ea6d8ae..dd9622f16a0 100644
--- a/spec/features/projects/gfm_autocomplete_load_spec.rb
+++ b/spec/features/projects/gfm_autocomplete_load_spec.rb
@@ -10,12 +10,12 @@ describe 'GFM autocomplete loading', feature: true, js: true do
end
it 'does not load on project#show' do
- expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).to eq('')
+ expect(evaluate_script('gl.GfmAutoComplete.dataSources')).to eq({})
end
it 'loads on new issue page' do
visit new_namespace_project_issue_path(project.namespace, project)
- expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).not_to eq('')
+ expect(evaluate_script('gl.GfmAutoComplete.dataSources')).not_to eq({})
end
end
diff --git a/spec/features/projects/guest_navigation_menu_spec.rb b/spec/features/projects/guest_navigation_menu_spec.rb
index c22441f8929..8120a51c515 100644
--- a/spec/features/projects/guest_navigation_menu_spec.rb
+++ b/spec/features/projects/guest_navigation_menu_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
describe "Guest navigation menu" do
- let(:project) { create :empty_project, :private }
- let(:guest) { create :user }
+ let(:project) { create(:empty_project, :private, public_builds: false) }
+ let(:guest) { create(:user) }
before do
project.team << [guest, :guest]
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 bfe59bdb90e..d3165d07d7b 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/labels/subscription_spec.rb b/spec/features/projects/labels/subscription_spec.rb
new file mode 100644
index 00000000000..3130d87fba5
--- /dev/null
+++ b/spec/features/projects/labels/subscription_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+feature 'Labels subscription', feature: true do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, :public, namespace: group) }
+ let!(:bug) { create(:label, project: project, title: 'bug') }
+ let!(:feature) { create(:group_label, group: group, title: 'feature') }
+
+ context 'when signed in' do
+ before do
+ project.team << [user, :developer]
+ login_as user
+ end
+
+ scenario 'users can subscribe/unsubscribe to labels', js: true do
+ visit namespace_project_labels_path(project.namespace, project)
+
+ expect(page).to have_content('bug')
+ expect(page).to have_content('feature')
+
+ within "#project_label_#{bug.id}" do
+ expect(page).not_to have_button 'Unsubscribe'
+
+ click_button 'Subscribe'
+
+ expect(page).not_to have_button 'Subscribe'
+ expect(page).to have_button 'Unsubscribe'
+
+ click_button 'Unsubscribe'
+
+ expect(page).to have_button 'Subscribe'
+ expect(page).not_to have_button 'Unsubscribe'
+ end
+
+ within "#group_label_#{feature.id}" do
+ expect(page).not_to have_button 'Unsubscribe'
+
+ click_link_on_dropdown('Group level')
+
+ expect(page).not_to have_selector('.dropdown-group-label')
+ expect(page).to have_button 'Unsubscribe'
+
+ click_button 'Unsubscribe'
+
+ expect(page).to have_selector('.dropdown-group-label')
+
+ click_link_on_dropdown('Project level')
+
+ expect(page).not_to have_selector('.dropdown-group-label')
+ expect(page).to have_button 'Unsubscribe'
+ end
+ end
+ end
+
+ context 'when not signed in' do
+ it 'users can not subscribe/unsubscribe to labels' do
+ visit namespace_project_labels_path(project.namespace, project)
+
+ expect(page).to have_content 'bug'
+ expect(page).to have_content 'feature'
+ expect(page).not_to have_button('Subscribe')
+ expect(page).not_to have_selector('.dropdown-group-label')
+ end
+ end
+
+ def click_link_on_dropdown(text)
+ find('.dropdown-group-label').click
+
+ page.within('.dropdown-group-label') do
+ find('a.js-subscribe-button', text: text).click
+ end
+ end
+end
diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/group_links_spec.rb
index cc2f695211c..94995f7cf95 100644
--- a/spec/features/projects/members/group_links_spec.rb
+++ b/spec/features/projects/members/group_links_spec.rb
@@ -16,12 +16,17 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t
end
it 'updates group access level' do
- select 'Guest', from: "member_access_level_#{group.id}"
+ click_button @group_link.human_access
+
+ page.within '.dropdown-menu' do
+ click_link 'Guest'
+ end
+
wait_for_ajax
visit namespace_project_project_members_path(project.namespace, project)
- expect(page).to have_select("member_access_level_#{group.id}", selected: 'Guest')
+ expect(first('.group_member')).to have_content('Guest')
end
it 'updates expiry date' do
diff --git a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
index 728c0e16361..b483ba4c54c 100644
--- a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
+++ b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
@@ -12,6 +12,6 @@ feature 'Projects > Members > Group member cannot leave group project', feature:
end
scenario 'user does not see a "Leave project" link' do
- expect(page).not_to have_content 'Leave Project'
+ expect(page).not_to have_content 'Leave project'
end
end
diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb
new file mode 100644
index 00000000000..7d0065ee2c4
--- /dev/null
+++ b/spec/features/projects/members/group_members_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+feature 'Projects members', feature: true do
+ let(:user) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:group) { create(:group, :public, :access_requestable) }
+ let(:project) { create(:empty_project, :public, :access_requestable, creator: user, group: group) }
+ let(:project_invitee) { create(:project_member, project: project, invite_token: '123', invite_email: 'test1@abc.com', user: nil) }
+ let(:group_invitee) { create(:group_member, group: group, invite_token: '123', invite_email: 'test2@abc.com', user: nil) }
+ let(:project_requester) { create(:user) }
+ let(:group_requester) { create(:user) }
+
+ background do
+ project.team << [developer, :developer]
+ group.add_owner(user)
+ login_as(user)
+ end
+
+ context 'with a group invitee' do
+ before do
+ group_invitee
+ visit namespace_project_project_members_path(project.namespace, project)
+ end
+
+ scenario 'does not appear in the project members page' do
+ page.within first('.content-list') do
+ expect(page).not_to have_content('test2@abc.com')
+ end
+ end
+ end
+
+ context 'with a group and a project invitee' do
+ before do
+ group_invitee
+ project_invitee
+ visit namespace_project_project_members_path(project.namespace, project)
+ end
+
+ scenario 'shows the project invitee, the project developer, and the group owner' do
+ page.within first('.content-list') do
+ expect(page).to have_content('test1@abc.com')
+ expect(page).not_to have_content('test2@abc.com')
+
+ # Project developer
+ expect(page).to have_content(developer.name)
+
+ # Group owner
+ expect(page).to have_content(user.name)
+ expect(page).to have_content(group.name)
+ end
+ end
+ end
+
+ context 'with a group requester' do
+ before do
+ group.request_access(group_requester)
+ visit namespace_project_project_members_path(project.namespace, project)
+ end
+
+ scenario 'does not appear in the project members page' do
+ page.within first('.content-list') do
+ expect(page).not_to have_content(group_requester.name)
+ end
+ end
+ end
+
+ context 'with a group and a project requesters' do
+ before do
+ group.request_access(group_requester)
+ project.request_access(project_requester)
+ visit namespace_project_project_members_path(project.namespace, project)
+ end
+
+ scenario 'shows the project requester, the project developer, and the group owner' do
+ page.within first('.content-list') do
+ expect(page).to have_content(project_requester.name)
+ expect(page).not_to have_content(group_requester.name)
+ end
+
+ page.within all('.content-list').last do
+ # Project developer
+ expect(page).to have_content(developer.name)
+
+ # Group owner
+ expect(page).to have_content(user.name)
+ expect(page).to have_content(group.name)
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
index c4ed92d2780..bdeeef57273 100644
--- a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
+++ b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
@@ -1,10 +1,10 @@
require 'spec_helper'
-feature 'Projects > Members > Group requester cannot request access to project', feature: true do
+feature 'Projects > Members > Group requester cannot request access to project', feature: true, js: true do
let(:user) { create(:user) }
let(:owner) { create(:user) }
- let(:group) { create(:group, :public) }
- let(:project) { create(:project, :public, namespace: group) }
+ let(:group) { create(:group, :public, :access_requestable) }
+ let(:project) { create(:project, :public, :access_requestable, namespace: group) }
background do
group.add_owner(owner)
diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb
index d15376931c3..143390b71cd 100644
--- a/spec/features/projects/members/master_manages_access_requests_spec.rb
+++ b/spec/features/projects/members/master_manages_access_requests_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
feature 'Projects > Members > Master manages access requests', feature: true do
let(:user) { create(:user) }
let(:master) { create(:user) }
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
background do
project.request_access(user)
diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb
index 79dec442818..5daa932e4e6 100644
--- a/spec/features/projects/members/member_leaves_project_spec.rb
+++ b/spec/features/projects/members/member_leaves_project_spec.rb
@@ -11,7 +11,7 @@ feature 'Projects > Members > Member leaves project', feature: true do
end
scenario 'user leaves project' do
- click_link 'Leave Project'
+ click_link 'Leave project'
expect(current_path).to eq(dashboard_projects_path)
expect(project.users.exists?(user.id)).to be_falsey
diff --git a/spec/features/projects/members/owner_cannot_leave_project_spec.rb b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
index 6e948b7a616..b26d55c5d5d 100644
--- a/spec/features/projects/members/owner_cannot_leave_project_spec.rb
+++ b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
@@ -8,7 +8,7 @@ feature 'Projects > Members > Owner cannot leave project', feature: true do
visit namespace_project_path(project.namespace, project)
end
- scenario 'user does not see a "Leave Project" link' do
- expect(page).not_to have_content 'Leave Project'
+ scenario 'user does not see a "Leave project" link' do
+ expect(page).not_to have_content 'Leave project'
end
end
diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb
new file mode 100644
index 00000000000..d6ebb523f95
--- /dev/null
+++ b/spec/features/projects/members/sorting_spec.rb
@@ -0,0 +1,98 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Sorting', feature: true do
+ let(:master) { create(:user, name: 'John Doe') }
+ let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) }
+ let(:project) { create(:empty_project) }
+
+ background do
+ create(:project_member, :master, user: master, project: project, created_at: 5.days.ago)
+ create(:project_member, :developer, user: developer, project: project, created_at: 3.days.ago)
+
+ login_as(master)
+ end
+
+ scenario 'sorts alphabetically by default' do
+ visit_members_list(sort: nil)
+
+ expect(first_member).to include(master.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
+ end
+
+ scenario 'sorts by access level ascending' do
+ visit_members_list(sort: :access_level_asc)
+
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(master.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
+ end
+
+ scenario 'sorts by access level descending' do
+ visit_members_list(sort: :access_level_desc)
+
+ expect(first_member).to include(master.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
+ end
+
+ scenario 'sorts by last joined' do
+ visit_members_list(sort: :last_joined)
+
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(master.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
+ end
+
+ scenario 'sorts by oldest joined' do
+ visit_members_list(sort: :oldest_joined)
+
+ expect(first_member).to include(master.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
+ end
+
+ scenario 'sorts by name ascending' do
+ visit_members_list(sort: :name_asc)
+
+ expect(first_member).to include(master.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
+ end
+
+ scenario 'sorts by name descending' do
+ visit_members_list(sort: :name_desc)
+
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(master.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
+ end
+
+ scenario 'sorts by recent sign in' do
+ visit_members_list(sort: :recent_sign_in)
+
+ expect(first_member).to include(master.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
+ end
+
+ scenario 'sorts by oldest sign in' do
+ visit_members_list(sort: :oldest_sign_in)
+
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(master.name)
+ expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in')
+ end
+
+ def visit_members_list(sort:)
+ visit namespace_project_project_members_path(project.namespace.to_param, project.to_param, sort: sort)
+ end
+
+ def first_member
+ page.all('ul.content-list > li').first.text
+ end
+
+ def second_member
+ page.all('ul.content-list > li').last.text
+ end
+end
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 56ede8eb5be..97c42bd7f01 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
feature 'Projects > Members > User requests access', feature: true do
let(:user) { create(:user) }
let(:master) { create(:user) }
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :access_requestable) }
background do
project.team << [master, :master]
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
new file mode 100644
index 00000000000..abfc46601fb
--- /dev/null
+++ b/spec/features/projects/new_project_spec.rb
@@ -0,0 +1,19 @@
+require "spec_helper"
+
+feature "New project", feature: true do
+ context "Visibility level selector" do
+ let(:user) { create(:admin) }
+
+ before { login_as(user) }
+
+ Gitlab::VisibilityLevel.options.each do |key, level|
+ it "sets selector to #{key}" do
+ stub_application_setting(default_project_visibility: level)
+
+ visit new_project_path
+
+ expect(find_field("project_visibility_level_#{level}")).to be_checked
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
new file mode 100644
index 00000000000..0a77eaa123c
--- /dev/null
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -0,0 +1,239 @@
+require 'spec_helper'
+
+describe "Pipelines", feature: true, js: true do
+ include GitlabRoutingHelper
+
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ before do
+ login_as(user)
+ project.team << [user, :developer]
+ end
+
+ describe 'GET /:project/pipelines/:id' do
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
+
+ before do
+ @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build')
+ @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test')
+ @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy')
+ @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build')
+ @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external')
+ end
+
+ before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) }
+
+ it 'shows the pipeline graph' do
+ expect(page).to have_selector('.pipeline-visualization')
+ expect(page).to have_content('Build')
+ expect(page).to have_content('Test')
+ expect(page).to have_content('Deploy')
+ expect(page).to have_content('Retry failed')
+ expect(page).to have_content('Cancel running')
+ end
+
+ it 'shows Pipeline tab pane as active' do
+ expect(page).to have_css('#js-tab-pipeline.active')
+ end
+
+ describe 'pipeline graph' do
+ context 'when pipeline has running builds' do
+ it 'shows a running icon and a cancel action for the running build' do
+ page.within('a[data-title="deploy - running"]') do
+ expect(page).to have_selector('.ci-status-icon-running')
+ expect(page).to have_content('deploy')
+ end
+
+ page.within('a[data-title="deploy - running"] + .ci-action-icon-container') do
+ expect(page).to have_selector('.ci-action-icon-container .fa-ban')
+ end
+ end
+
+ it 'should be possible to cancel the running build' do
+ find('a[data-title="deploy - running"] + .ci-action-icon-container').trigger('click')
+
+ expect(page).not_to have_content('Cancel running')
+ end
+ end
+
+ context 'when pipeline has successful builds' do
+ it 'shows the success icon and a retry action for the successfull build' do
+ page.within('a[data-title="build - passed"]') do
+ expect(page).to have_selector('.ci-status-icon-success')
+ expect(page).to have_content('build')
+ end
+
+ page.within('a[data-title="build - passed"] + .ci-action-icon-container') do
+ expect(page).to have_selector('.ci-action-icon-container .fa-refresh')
+ end
+ end
+
+ it 'should be possible to retry the success build' do
+ find('a[data-title="build - passed"] + .ci-action-icon-container').trigger('click')
+
+ expect(page).not_to have_content('Retry build')
+ 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('a[data-title="test - failed"]') do
+ expect(page).to have_selector('.ci-status-icon-failed')
+ expect(page).to have_content('test')
+ end
+
+ page.within('a[data-title="test - failed"] + .ci-action-icon-container') do
+ expect(page).to have_selector('.ci-action-icon-container .fa-refresh')
+ end
+ end
+
+ it 'should be possible to retry the failed build' do
+ find('a[data-title="test - failed"] + .ci-action-icon-container').trigger('click')
+
+ expect(page).not_to have_content('Retry build')
+ end
+ end
+
+ context 'when pipeline has manual builds' do
+ it 'shows the skipped icon and a play action for the manual build' do
+ page.within('a[data-title="manual build - manual play action"]') do
+ expect(page).to have_selector('.ci-status-icon-skipped')
+ expect(page).to have_content('manual')
+ end
+
+ page.within('a[data-title="manual build - manual play action"] + .ci-action-icon-container') do
+ expect(page).to have_selector('.ci-action-icon-container .fa-play')
+ end
+ end
+
+ it 'should be possible to play the manual build' do
+ find('a[data-title="manual build - manual play action"] + .ci-action-icon-container').trigger('click')
+
+ expect(page).not_to have_content('Play build')
+ end
+ end
+
+ context 'when pipeline has external build' do
+ it 'shows the success icon and the generic comit status build' do
+ expect(page).to have_selector('.ci-status-icon-success')
+ expect(page).to have_content('jenkins')
+ end
+ end
+ end
+
+ context 'page tabs' do
+ it 'shows Pipeline and Builds tabs with link' do
+ expect(page).to have_link('Pipeline')
+ expect(page).to have_link('Builds')
+ end
+
+ it 'shows counter in Builds tab' do
+ expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s)
+ end
+
+ it 'shows Pipeline tab as active' do
+ expect(page).to have_css('.js-pipeline-tab-link.active')
+ end
+ end
+
+ context 'retrying builds' do
+ it { expect(page).not_to have_content('retried') }
+
+ context 'when retrying' do
+ before { click_on 'Retry failed' }
+
+ it { expect(page).not_to have_content('Retry failed') }
+ end
+ end
+
+ context 'canceling builds' do
+ it { expect(page).not_to have_selector('.ci-canceled') }
+
+ context 'when canceling' do
+ before { click_on 'Cancel running' }
+
+ it { expect(page).not_to have_content('Cancel running') }
+ end
+ end
+ end
+
+ describe 'GET /:project/pipelines/:id/builds' do
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
+
+ before do
+ @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build')
+ @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test')
+ @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy')
+ @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build')
+ @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external')
+ end
+
+ before { visit builds_namespace_project_pipeline_path(project.namespace, project, pipeline)}
+
+ it 'shows a list of builds' do
+ expect(page).to have_content('Test')
+ expect(page).to have_content(@success.id)
+ expect(page).to have_content('Deploy')
+ expect(page).to have_content(@failed.id)
+ expect(page).to have_content(@running.id)
+ expect(page).to have_content(@external.id)
+ expect(page).to have_content('Retry failed')
+ expect(page).to have_content('Cancel running')
+ expect(page).to have_link('Play')
+ end
+
+ it 'shows Builds 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
+ expect(page).to have_link('Pipeline')
+ expect(page).to have_link('Builds')
+ end
+
+ it 'shows counter in Builds tab' do
+ expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s)
+ end
+
+ it 'shows Builds tab as active' do
+ expect(page).to have_css('li.js-builds-tab-link.active')
+ end
+ end
+
+ context 'retrying builds' do
+ it { expect(page).not_to have_content('retried') }
+
+ context 'when retrying' do
+ before { click_on 'Retry failed' }
+
+ it { expect(page).not_to have_content('Retry failed') }
+ it { expect(page).to have_selector('.retried') }
+ end
+ end
+
+ context 'canceling builds' do
+ it { expect(page).not_to have_selector('.ci-canceled') }
+
+ context 'when canceling' do
+ before { click_on 'Cancel running' }
+
+ it { expect(page).not_to have_content('Cancel running') }
+ it { expect(page).to have_selector('.ci-canceled') }
+ end
+ end
+
+ context 'playing manual build' do
+ before do
+ within '.pipeline-holder' do
+ click_link('Play')
+ end
+ end
+
+ it { expect(@manual.reload).to be_pending }
+ end
+ end
+end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
new file mode 100644
index 00000000000..f3731698a18
--- /dev/null
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -0,0 +1,211 @@
+require 'spec_helper'
+
+describe "Pipelines" do
+ include GitlabRoutingHelper
+
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ before do
+ login_as(user)
+ project.team << [user, :developer]
+ end
+
+ describe 'GET /:project/pipelines' do
+ let!(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running') }
+
+ [:all, :running, :branches].each do |scope|
+ context "displaying #{scope}" do
+ let(:project) { create(:project) }
+
+ before { visit namespace_project_pipelines_path(project.namespace, project, scope: scope) }
+
+ it { expect(page).to have_content(pipeline.short_sha) }
+ end
+ end
+
+ context 'anonymous access' do
+ before { visit namespace_project_pipelines_path(project.namespace, project) }
+
+ it { expect(page).to have_http_status(:success) }
+ end
+
+ context 'cancelable pipeline' do
+ let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') }
+
+ before do
+ build.run
+ visit namespace_project_pipelines_path(project.namespace, project)
+ end
+
+ it { expect(page).to have_link('Cancel') }
+ it { expect(page).to have_selector('.ci-running') }
+
+ context 'when canceling' do
+ before { click_link('Cancel') }
+
+ it { expect(page).not_to have_link('Cancel') }
+ it { expect(page).to have_selector('.ci-canceled') }
+ end
+ end
+
+ context 'retryable pipelines' do
+ let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') }
+
+ before do
+ build.drop
+ visit namespace_project_pipelines_path(project.namespace, project)
+ end
+
+ it { expect(page).to have_link('Retry') }
+ it { expect(page).to have_selector('.ci-failed') }
+
+ context 'when retrying' do
+ before { click_link('Retry') }
+
+ it { expect(page).not_to have_link('Retry') }
+ it { expect(page).to have_selector('.ci-running') }
+ end
+ end
+
+ context 'with manual actions' do
+ let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'manual build', stage: 'test', commands: 'test') }
+
+ before { visit namespace_project_pipelines_path(project.namespace, project) }
+
+ it { expect(page).to have_link('Manual build') }
+
+ context 'when playing' do
+ before { click_link('Manual build') }
+
+ it { expect(manual.reload).to be_pending }
+ end
+ end
+
+ context 'for generic statuses' do
+ context 'when running' do
+ let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') }
+
+ before do
+ visit namespace_project_pipelines_path(project.namespace, project)
+ end
+
+ it 'is cancelable' do
+ expect(page).to have_link('Cancel')
+ end
+
+ it 'has pipeline running' do
+ expect(page).to have_selector('.ci-running')
+ end
+
+ context 'when canceling' do
+ before { click_link('Cancel') }
+
+ it { expect(page).not_to have_link('Cancel') }
+ it { expect(page).to have_selector('.ci-canceled') }
+ end
+ end
+
+ context 'when failed' do
+ let!(:status) { create(:generic_commit_status, :pending, pipeline: pipeline, stage: 'test') }
+
+ before do
+ status.drop
+ visit namespace_project_pipelines_path(project.namespace, project)
+ end
+
+ it 'is not retryable' do
+ expect(page).not_to have_link('Retry')
+ end
+
+ it 'has failed pipeline' do
+ expect(page).to have_selector('.ci-failed')
+ end
+ end
+ end
+
+ context 'downloadable pipelines' do
+ context 'with artifacts' do
+ let!(:with_artifacts) { create(:ci_build, :artifacts, :success, pipeline: pipeline, name: 'rspec tests', stage: 'test') }
+
+ before { visit namespace_project_pipelines_path(project.namespace, project) }
+
+ it { expect(page).to have_selector('.build-artifacts') }
+ it { expect(page).to have_link(with_artifacts.name) }
+ end
+
+ context 'with artifacts expired' do
+ let!(:with_artifacts_expired) { create(:ci_build, :artifacts_expired, :success, pipeline: pipeline, name: 'rspec', stage: 'test') }
+
+ before { visit namespace_project_pipelines_path(project.namespace, project) }
+
+ it { expect(page).not_to have_selector('.build-artifacts') }
+ end
+
+ context 'without artifacts' do
+ let!(:without_artifacts) { create(:ci_build, :success, pipeline: pipeline, name: 'rspec', stage: 'test') }
+
+ before { visit namespace_project_pipelines_path(project.namespace, project) }
+
+ it { expect(page).not_to have_selector('.build-artifacts') }
+ end
+ end
+ end
+
+ describe 'POST /:project/pipelines' do
+ let(:project) { create(:project) }
+
+ before { visit new_namespace_project_pipeline_path(project.namespace, project) }
+
+ context 'for valid commit' do
+ before { fill_in('pipeline[ref]', with: 'master') }
+
+ context 'with gitlab-ci.yml' do
+ before { stub_ci_pipeline_to_return_yaml_file }
+
+ it { expect{ click_on 'Create pipeline' }.to change{ Ci::Pipeline.count }.by(1) }
+ end
+
+ context 'without gitlab-ci.yml' do
+ before { click_on 'Create pipeline' }
+
+ it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
+ end
+ end
+
+ context 'for invalid commit' do
+ before do
+ fill_in('pipeline[ref]', with: 'invalid-reference')
+ click_on 'Create pipeline'
+ end
+
+ it { expect(page).to have_content('Reference not found') }
+ end
+ end
+
+ describe 'Create pipelines', feature: true do
+ let(:project) { create(:project) }
+
+ before do
+ visit new_namespace_project_pipeline_path(project.namespace, project)
+ end
+
+ describe 'new pipeline page' do
+ it 'has field to add a new pipeline' do
+ expect(page).to have_field('pipeline[ref]')
+ expect(page).to have_content('Create for')
+ end
+ end
+
+ describe 'find pipelines' do
+ it 'shows filtered pipelines', js: true do
+ fill_in('pipeline[ref]', with: 'fix')
+ find('input#ref').native.send_keys(:keydown)
+
+ within('.ui-autocomplete') do
+ expect(page).to have_selector('li', text: 'fix')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb
deleted file mode 100644
index db56a50e058..00000000000
--- a/spec/features/projects/pipelines_spec.rb
+++ /dev/null
@@ -1,262 +0,0 @@
-require 'spec_helper'
-
-describe "Pipelines" do
- include GitlabRoutingHelper
-
- let(:project) { create(:empty_project) }
- let(:user) { create(:user) }
-
- before do
- login_as(user)
- project.team << [user, :developer]
- end
-
- describe 'GET /:project/pipelines' do
- let!(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running') }
-
- [:all, :running, :branches].each do |scope|
- context "displaying #{scope}" do
- let(:project) { create(:project) }
-
- before { visit namespace_project_pipelines_path(project.namespace, project, scope: scope) }
-
- it { expect(page).to have_content(pipeline.short_sha) }
- end
- end
-
- context 'anonymous access' do
- before { visit namespace_project_pipelines_path(project.namespace, project) }
-
- it { expect(page).to have_http_status(:success) }
- end
-
- context 'cancelable pipeline' do
- let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') }
-
- before do
- build.run
- visit namespace_project_pipelines_path(project.namespace, project)
- end
-
- it { expect(page).to have_link('Cancel') }
- it { expect(page).to have_selector('.ci-running') }
-
- context 'when canceling' do
- before { click_link('Cancel') }
-
- it { expect(page).not_to have_link('Cancel') }
- it { expect(page).to have_selector('.ci-canceled') }
- end
- end
-
- context 'retryable pipelines' do
- let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') }
-
- before do
- build.drop
- visit namespace_project_pipelines_path(project.namespace, project)
- end
-
- it { expect(page).to have_link('Retry') }
- it { expect(page).to have_selector('.ci-failed') }
-
- context 'when retrying' do
- before { click_link('Retry') }
-
- it { expect(page).not_to have_link('Retry') }
- it { expect(page).to have_selector('.ci-running') }
- end
- end
-
- context 'with manual actions' do
- let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'manual build', stage: 'test', commands: 'test') }
-
- before { visit namespace_project_pipelines_path(project.namespace, project) }
-
- it { expect(page).to have_link('Manual build') }
-
- context 'when playing' do
- before { click_link('Manual build') }
-
- it { expect(manual.reload).to be_pending }
- end
- end
-
- context 'for generic statuses' do
- context 'when running' do
- let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') }
-
- before do
- visit namespace_project_pipelines_path(project.namespace, project)
- end
-
- it 'is not cancelable' do
- expect(page).not_to have_link('Cancel')
- end
-
- it 'has pipeline running' do
- expect(page).to have_selector('.ci-running')
- end
- end
-
- context 'when failed' do
- let!(:status) { create(:generic_commit_status, :pending, pipeline: pipeline, stage: 'test') }
-
- before do
- status.drop
- visit namespace_project_pipelines_path(project.namespace, project)
- end
-
- it 'is not retryable' do
- expect(page).not_to have_link('Retry')
- end
-
- it 'has failed pipeline' do
- expect(page).to have_selector('.ci-failed')
- end
- end
- end
-
- context 'downloadable pipelines' do
- context 'with artifacts' do
- let!(:with_artifacts) { create(:ci_build, :artifacts, :success, pipeline: pipeline, name: 'rspec tests', stage: 'test') }
-
- before { visit namespace_project_pipelines_path(project.namespace, project) }
-
- it { expect(page).to have_selector('.build-artifacts') }
- it { expect(page).to have_link(with_artifacts.name) }
- end
-
- context 'with artifacts expired' do
- let!(:with_artifacts_expired) { create(:ci_build, :artifacts_expired, :success, pipeline: pipeline, name: 'rspec', stage: 'test') }
-
- before { visit namespace_project_pipelines_path(project.namespace, project) }
-
- it { expect(page).not_to have_selector('.build-artifacts') }
- end
-
- context 'without artifacts' do
- let!(:without_artifacts) { create(:ci_build, :success, pipeline: pipeline, name: 'rspec', stage: 'test') }
-
- before { visit namespace_project_pipelines_path(project.namespace, project) }
-
- it { expect(page).not_to have_selector('.build-artifacts') }
- end
- end
- end
-
- describe 'GET /:project/pipelines/:id' do
- let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
-
- before do
- @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build')
- @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test')
- @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy')
- @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build')
- @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external')
- end
-
- before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) }
-
- it 'shows a list of builds' do
- expect(page).to have_content('Test')
- expect(page).to have_content(@success.id)
- expect(page).to have_content('Deploy')
- expect(page).to have_content(@failed.id)
- expect(page).to have_content(@running.id)
- expect(page).to have_content(@external.id)
- expect(page).to have_content('Retry failed')
- expect(page).to have_content('Cancel running')
- expect(page).to have_link('Play')
- end
-
- context 'retrying builds' do
- it { expect(page).not_to have_content('retried') }
-
- context 'when retrying' do
- before { click_on 'Retry failed' }
-
- it { expect(page).not_to have_content('Retry failed') }
- it { expect(page).to have_selector('.retried') }
- end
- end
-
- context 'canceling builds' do
- it { expect(page).not_to have_selector('.ci-canceled') }
-
- context 'when canceling' do
- before { click_on 'Cancel running' }
-
- it { expect(page).not_to have_content('Cancel running') }
- it { expect(page).to have_selector('.ci-canceled') }
- end
- end
-
- context 'playing manual build' do
- before do
- within '.pipeline-holder' do
- click_link('Play')
- end
- end
-
- it { expect(@manual.reload).to be_pending }
- end
- end
-
- describe 'POST /:project/pipelines' do
- let(:project) { create(:project) }
-
- before { visit new_namespace_project_pipeline_path(project.namespace, project) }
-
- context 'for valid commit' do
- before { fill_in('pipeline[ref]', with: 'master') }
-
- context 'with gitlab-ci.yml' do
- before { stub_ci_pipeline_to_return_yaml_file }
-
- it { expect{ click_on 'Create pipeline' }.to change{ Ci::Pipeline.count }.by(1) }
- end
-
- context 'without gitlab-ci.yml' do
- before { click_on 'Create pipeline' }
-
- it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
- end
- end
-
- context 'for invalid commit' do
- before do
- fill_in('pipeline[ref]', with: 'invalid-reference')
- click_on 'Create pipeline'
- end
-
- it { expect(page).to have_content('Reference not found') }
- end
- end
-
- describe 'Create pipelines', feature: true do
- let(:project) { create(:project) }
-
- before do
- visit new_namespace_project_pipeline_path(project.namespace, project)
- end
-
- describe 'new pipeline page' do
- it 'has field to add a new pipeline' do
- expect(page).to have_field('pipeline[ref]')
- expect(page).to have_content('Create for')
- end
- end
-
- describe 'find pipelines' do
- it 'shows filtered pipelines', js: true do
- fill_in('pipeline[ref]', with: 'fix')
- find('input#ref').native.send_keys(:keydown)
-
- within('.ui-autocomplete') do
- expect(page).to have_selector('li', text: 'fix')
- end
- end
- end
- end
-end
diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb
index 3de25d7af7d..bf60cca4ea4 100644
--- a/spec/features/projects/project_settings_spec.rb
+++ b/spec/features/projects/project_settings_spec.rb
@@ -18,7 +18,7 @@ describe 'Edit Project Settings', feature: true do
click_button 'Save changes'
expect(page).to have_field 'project_name_edit', with: 'foo&bar'
- expect(page).to have_content "Name can contain only letters, digits, '_', '.', dash and space. It must start with letter, digit or '_'."
+ expect(page).to have_content "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'."
expect(page).to have_button 'Save changes'
end
end
@@ -34,8 +34,21 @@ describe 'Edit Project Settings', feature: true do
expect(page).to have_field 'Project name', with: 'foo&bar'
expect(page).to have_field 'Path', with: 'foo&bar'
- expect(page).to have_content "Name can contain only letters, digits, '_', '.', dash and space. It must start with letter, digit or '_'."
+ expect(page).to have_content "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'."
expect(page).to have_content "Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'"
end
end
+
+ describe 'Rename repository name with emojis' do
+ it 'shows error for invalid project name' do
+ visit edit_namespace_project_path(project.namespace, project)
+
+ fill_in 'Project name', with: '🚀 foo bar ☁️'
+
+ click_button 'Rename project'
+
+ expect(page).to have_field 'Project name', with: '🚀 foo bar ☁️'
+ expect(page).not_to have_content "Name can contain only letters, digits, emojis '_', '.', dash and space. It must start with letter, digit, emoji or '_'."
+ end
+ end
end
diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb
new file mode 100644
index 00000000000..f474e7e891b
--- /dev/null
+++ b/spec/features/projects/services/mattermost_slash_command_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+feature 'Setup Mattermost slash commands', feature: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:service) { project.create_mattermost_slash_commands_service }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ describe 'user visites the mattermost slash command config page', js: true do
+ it 'shows a help message' do
+ visit edit_namespace_project_service_path(project.namespace, project, service)
+
+ wait_for_ajax
+
+ expect(page).to have_content("This service allows GitLab users to perform common")
+ end
+ end
+
+ describe 'saving a token' do
+ let(:token) { ('a'..'z').to_a.join }
+
+ it 'shows the token after saving' do
+ visit edit_namespace_project_service_path(project.namespace, project, service)
+
+ fill_in 'service_token', with: token
+ click_on 'Save'
+
+ value = find_field('service_token').value
+
+ expect(value).to eq(token)
+ end
+ end
+
+ describe 'the trigger url' do
+ it 'shows the correct url' do
+ visit edit_namespace_project_service_path(project.namespace, project, service)
+
+ value = find_field('request_url').value
+ expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger")
+ end
+ end
+end
diff --git a/spec/features/projects/services/slack_service_spec.rb b/spec/features/projects/services/slack_service_spec.rb
new file mode 100644
index 00000000000..320ed13a01d
--- /dev/null
+++ b/spec/features/projects/services/slack_service_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+feature 'Projects > Slack service > Setup events', feature: true do
+ let(:user) { create(:user) }
+ let(:service) { SlackNotificationService.new }
+ let(:project) { create(:project, slack_notification_service: service) }
+
+ background do
+ service.fields
+ service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, build_channel: 6, wiki_page_channel: 7)
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ scenario 'user can filter events by channel' do
+ visit edit_namespace_project_service_path(project.namespace, project, service)
+
+ expect(page.find_field("service_push_channel").value).to have_content '1'
+ expect(page.find_field("service_issue_channel").value).to have_content '2'
+ expect(page.find_field("service_merge_request_channel").value).to have_content '3'
+ expect(page.find_field("service_note_channel").value).to have_content '4'
+ expect(page.find_field("service_tag_push_channel").value).to have_content '5'
+ expect(page.find_field("service_build_channel").value).to have_content '6'
+ expect(page.find_field("service_wiki_page_channel").value).to have_content '7'
+ end
+end
diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb
new file mode 100644
index 00000000000..4bfaa499272
--- /dev/null
+++ b/spec/features/projects/settings/merge_requests_settings_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+feature 'Project settings > Merge Requests', feature: true, js: true do
+ include GitlabRoutingHelper
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+
+ background do
+ project.team << [user, :master]
+ 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
+ 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 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 all discussions are resolved')
+ end
+ end
+
+ context 'when Builds 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).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 all discussions are resolved')
+ end
+ end
+ end
+
+ context 'when Merge Request are initially disabled' do
+ before do
+ project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::DISABLED)
+ visit edit_project_path(project)
+ 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 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 all discussions are resolved')
+ end
+ end
+end
diff --git a/spec/features/projects/slack_service/slack_service_spec.rb b/spec/features/projects/slack_service/slack_service_spec.rb
deleted file mode 100644
index 16541f51d98..00000000000
--- a/spec/features/projects/slack_service/slack_service_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require 'spec_helper'
-
-feature 'Projects > Slack service > Setup events', feature: true do
- let(:user) { create(:user) }
- let(:service) { SlackService.new }
- let(:project) { create(:project, slack_service: service) }
-
- background do
- service.fields
- service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, build_channel: 6, wiki_page_channel: 7)
- project.team << [user, :master]
- login_as(user)
- end
-
- scenario 'user can filter events by channel' do
- visit edit_namespace_project_service_path(project.namespace, project, service)
-
- expect(page.find_field("service_push_channel").value).to have_content '1'
- expect(page.find_field("service_issue_channel").value).to have_content '2'
- expect(page.find_field("service_merge_request_channel").value).to have_content '3'
- expect(page.find_field("service_note_channel").value).to have_content '4'
- expect(page.find_field("service_tag_push_channel").value).to have_content '5'
- expect(page.find_field("service_build_channel").value).to have_content '6'
- expect(page.find_field("service_wiki_page_channel").value).to have_content '7'
- end
-end
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index 7afd83b7250..fff8b9f3447 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -20,7 +20,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
click_button 'Create page'
expect(page).to have_content('Home')
- expect(page).to have_content("last edited by #{user.name}")
+ expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!')
end
end
@@ -41,7 +41,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
click_button 'Create page'
expect(page).to have_content('Foo')
- expect(page).to have_content("last edited by #{user.name}")
+ expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!')
end
@@ -55,7 +55,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
click_button 'Create page'
expect(page).to have_content('Spaces in the name')
- expect(page).to have_content("last edited by #{user.name}")
+ expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!')
end
@@ -69,7 +69,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
click_button 'Create page'
expect(page).to have_content('Hyphens in the name')
- expect(page).to have_content("last edited by #{user.name}")
+ expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!')
end
end
@@ -85,7 +85,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
click_button 'Create page'
expect(page).to have_content('Home')
- expect(page).to have_content("last edited by #{user.name}")
+ expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!')
end
end
@@ -105,7 +105,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
click_button 'Create page'
expect(page).to have_content('Foo')
- expect(page).to have_content("last edited by #{user.name}")
+ expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!')
end
end
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index ef82d2375dd..f842d14fa96 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -22,7 +22,7 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do
click_button 'Save changes'
expect(page).to have_content('Home')
- expect(page).to have_content("last edited by #{user.name}")
+ expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!')
end
end
@@ -37,7 +37,7 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do
click_button 'Save changes'
expect(page).to have_content('Home')
- expect(page).to have_content("last edited by #{user.name}")
+ expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!')
end
end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index 1806200c82c..caecd027aaa 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -100,6 +100,32 @@ describe "Search", feature: true do
expect(page).to have_link(snippet.title)
end
+
+ it 'finds a commit' do
+ visit namespace_project_path(project.namespace, project)
+
+ page.within '.search' do
+ fill_in 'search', with: 'add'
+ click_button 'Go'
+ end
+
+ click_link "Commits"
+
+ expect(page).to have_selector('.commit-row-description')
+ end
+
+ it 'finds a code' do
+ visit namespace_project_path(project.namespace, project)
+
+ page.within '.search' do
+ fill_in 'search', with: 'def'
+ click_button 'Go'
+ end
+
+ click_link "Code"
+
+ expect(page).to have_selector('.file-content .code')
+ end
end
describe 'Right header search field', feature: true do
diff --git a/spec/features/security/admin_access_spec.rb b/spec/features/security/admin_access_spec.rb
index fe8cd7b7602..e180ca53eb5 100644
--- a/spec/features/security/admin_access_spec.rb
+++ b/spec/features/security/admin_access_spec.rb
@@ -4,7 +4,7 @@ describe "Admin::Projects", feature: true do
include AccessMatchers
describe "GET /admin/projects" do
- subject { admin_namespaces_projects_path }
+ subject { admin_projects_path }
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for :user }
diff --git a/spec/features/security/group/internal_access_spec.rb b/spec/features/security/group/internal_access_spec.rb
index 35fcef7a712..87cce32d6c6 100644
--- a/spec/features/security/group/internal_access_spec.rb
+++ b/spec/features/security/group/internal_access_spec.rb
@@ -3,25 +3,12 @@ require 'rails_helper'
describe 'Internal Group access', feature: true do
include AccessMatchers
- let(:group) { create(:group, :internal) }
+ let(:group) { create(:group, :internal) }
let(:project) { create(:project, :internal, group: group) }
-
- let(:owner) { create(:user) }
- let(:master) { create(:user) }
- let(:developer) { create(:user) }
- let(:reporter) { create(:user) }
- let(:guest) { create(:user) }
-
- let(:project_guest) { create(:user) }
-
- before do
- group.add_owner(owner)
- group.add_master(master)
- group.add_developer(developer)
- group.add_reporter(reporter)
- group.add_guest(guest)
-
- project.team << [project_guest, :guest]
+ let(:project_guest) do
+ create(:user) do |user|
+ project.add_guest(user)
+ end
end
describe "Group should be internal" do
@@ -34,75 +21,75 @@ describe 'Internal Group access', feature: true do
describe 'GET /groups/:path' do
subject { group_path(group) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for project_guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(group) }
+ it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:developer).of(group) }
+ it { is_expected.to be_allowed_for(:reporter).of(group) }
+ it { is_expected.to be_allowed_for(:guest).of(group) }
+ it { is_expected.to be_allowed_for(project_guest) }
+ it { is_expected.to be_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe 'GET /groups/:path/issues' do
subject { issues_group_path(group) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for project_guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(group) }
+ it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:developer).of(group) }
+ it { is_expected.to be_allowed_for(:reporter).of(group) }
+ it { is_expected.to be_allowed_for(:guest).of(group) }
+ it { is_expected.to be_allowed_for(project_guest) }
+ it { is_expected.to be_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe 'GET /groups/:path/merge_requests' do
subject { merge_requests_group_path(group) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for project_guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(group) }
+ it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:developer).of(group) }
+ it { is_expected.to be_allowed_for(:reporter).of(group) }
+ it { is_expected.to be_allowed_for(:guest).of(group) }
+ it { is_expected.to be_allowed_for(project_guest) }
+ it { is_expected.to be_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe 'GET /groups/:path/group_members' do
subject { group_group_members_path(group) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for project_guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(group) }
+ it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:developer).of(group) }
+ it { is_expected.to be_allowed_for(:reporter).of(group) }
+ it { is_expected.to be_allowed_for(:guest).of(group) }
+ it { is_expected.to be_allowed_for(project_guest) }
+ it { is_expected.to be_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe 'GET /groups/:path/edit' do
subject { edit_group_path(group) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_denied_for master }
- it { is_expected.to be_denied_for developer }
- it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for project_guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :visitor }
- it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(group) }
+ it { is_expected.to be_denied_for(:master).of(group) }
+ it { is_expected.to be_denied_for(:developer).of(group) }
+ it { is_expected.to be_denied_for(:reporter).of(group) }
+ it { is_expected.to be_denied_for(:guest).of(group) }
+ it { is_expected.to be_denied_for(project_guest) }
+ 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
end
diff --git a/spec/features/security/group/private_access_spec.rb b/spec/features/security/group/private_access_spec.rb
index 75a93342628..1d6b3e77c22 100644
--- a/spec/features/security/group/private_access_spec.rb
+++ b/spec/features/security/group/private_access_spec.rb
@@ -3,25 +3,12 @@ require 'rails_helper'
describe 'Private Group access', feature: true do
include AccessMatchers
- let(:group) { create(:group, :private) }
+ let(:group) { create(:group, :private) }
let(:project) { create(:project, :private, group: group) }
-
- let(:owner) { create(:user) }
- let(:master) { create(:user) }
- let(:developer) { create(:user) }
- let(:reporter) { create(:user) }
- let(:guest) { create(:user) }
-
- let(:project_guest) { create(:user) }
-
- before do
- group.add_owner(owner)
- group.add_master(master)
- group.add_developer(developer)
- group.add_reporter(reporter)
- group.add_guest(guest)
-
- project.team << [project_guest, :guest]
+ let(:project_guest) do
+ create(:user) do |user|
+ project.add_guest(user)
+ end
end
describe "Group should be private" do
@@ -34,75 +21,75 @@ describe 'Private Group access', feature: true do
describe 'GET /groups/:path' do
subject { group_path(group) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for project_guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(group) }
+ it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:developer).of(group) }
+ it { is_expected.to be_allowed_for(:reporter).of(group) }
+ it { is_expected.to be_allowed_for(:guest).of(group) }
+ it { is_expected.to be_allowed_for(project_guest) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe 'GET /groups/:path/issues' do
subject { issues_group_path(group) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for project_guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(group) }
+ it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:developer).of(group) }
+ it { is_expected.to be_allowed_for(:reporter).of(group) }
+ it { is_expected.to be_allowed_for(:guest).of(group) }
+ it { is_expected.to be_allowed_for(project_guest) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe 'GET /groups/:path/merge_requests' do
subject { merge_requests_group_path(group) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for project_guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(group) }
+ it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:developer).of(group) }
+ it { is_expected.to be_allowed_for(:reporter).of(group) }
+ it { is_expected.to be_allowed_for(:guest).of(group) }
+ it { is_expected.to be_allowed_for(project_guest) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe 'GET /groups/:path/group_members' do
subject { group_group_members_path(group) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for project_guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(group) }
+ it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:developer).of(group) }
+ it { is_expected.to be_allowed_for(:reporter).of(group) }
+ it { is_expected.to be_allowed_for(:guest).of(group) }
+ it { is_expected.to be_allowed_for(project_guest) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe 'GET /groups/:path/edit' do
subject { edit_group_path(group) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_denied_for master }
- it { is_expected.to be_denied_for developer }
- it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for project_guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :visitor }
- it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(group) }
+ it { is_expected.to be_denied_for(:master).of(group) }
+ it { is_expected.to be_denied_for(:developer).of(group) }
+ it { is_expected.to be_denied_for(:reporter).of(group) }
+ it { is_expected.to be_denied_for(:guest).of(group) }
+ it { is_expected.to be_denied_for(project_guest) }
+ 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
end
diff --git a/spec/features/security/group/public_access_spec.rb b/spec/features/security/group/public_access_spec.rb
index 6c5ee93970b..d7d76177269 100644
--- a/spec/features/security/group/public_access_spec.rb
+++ b/spec/features/security/group/public_access_spec.rb
@@ -3,25 +3,12 @@ require 'rails_helper'
describe 'Public Group access', feature: true do
include AccessMatchers
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public) }
let(:project) { create(:project, :public, group: group) }
-
- let(:owner) { create(:user) }
- let(:master) { create(:user) }
- let(:developer) { create(:user) }
- let(:reporter) { create(:user) }
- let(:guest) { create(:user) }
-
- let(:project_guest) { create(:user) }
-
- before do
- group.add_owner(owner)
- group.add_master(master)
- group.add_developer(developer)
- group.add_reporter(reporter)
- group.add_guest(guest)
-
- project.team << [project_guest, :guest]
+ let(:project_guest) do
+ create(:user) do |user|
+ project.add_guest(user)
+ end
end
describe "Group should be public" do
@@ -34,75 +21,75 @@ describe 'Public Group access', feature: true do
describe 'GET /groups/:path' do
subject { group_path(group) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for project_guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(group) }
+ it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:developer).of(group) }
+ it { is_expected.to be_allowed_for(:reporter).of(group) }
+ it { is_expected.to be_allowed_for(:guest).of(group) }
+ it { is_expected.to be_allowed_for(project_guest) }
+ it { is_expected.to be_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
describe 'GET /groups/:path/issues' do
subject { issues_group_path(group) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for project_guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(group) }
+ it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:developer).of(group) }
+ it { is_expected.to be_allowed_for(:reporter).of(group) }
+ it { is_expected.to be_allowed_for(:guest).of(group) }
+ it { is_expected.to be_allowed_for(project_guest) }
+ it { is_expected.to be_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
describe 'GET /groups/:path/merge_requests' do
subject { merge_requests_group_path(group) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for project_guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(group) }
+ it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:developer).of(group) }
+ it { is_expected.to be_allowed_for(:reporter).of(group) }
+ it { is_expected.to be_allowed_for(:guest).of(group) }
+ it { is_expected.to be_allowed_for(project_guest) }
+ it { is_expected.to be_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
describe 'GET /groups/:path/group_members' do
subject { group_group_members_path(group) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for project_guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(group) }
+ it { is_expected.to be_allowed_for(:master).of(group) }
+ it { is_expected.to be_allowed_for(:developer).of(group) }
+ it { is_expected.to be_allowed_for(:reporter).of(group) }
+ it { is_expected.to be_allowed_for(:guest).of(group) }
+ it { is_expected.to be_allowed_for(project_guest) }
+ it { is_expected.to be_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
describe 'GET /groups/:path/edit' do
subject { edit_group_path(group) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_denied_for master }
- it { is_expected.to be_denied_for developer }
- it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for project_guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :visitor }
- it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(group) }
+ it { is_expected.to be_denied_for(:master).of(group) }
+ it { is_expected.to be_denied_for(:developer).of(group) }
+ it { is_expected.to be_denied_for(:reporter).of(group) }
+ it { is_expected.to be_denied_for(:guest).of(group) }
+ it { is_expected.to be_denied_for(project_guest) }
+ 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
end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index b6acc509342..1897c8119d2 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -5,19 +5,6 @@ describe "Internal Project Access", feature: true do
let(:project) { create(:project, :internal) }
- let(:owner) { project.owner }
- let(:master) { create(:user) }
- let(:developer) { create(:user) }
- let(:reporter) { create(:user) }
- let(:guest) { create(:user) }
-
- before do
- project.team << [master, :master]
- project.team << [developer, :developer]
- project.team << [reporter, :reporter]
- project.team << [guest, :guest]
- end
-
describe "Project should be internal" do
describe '#internal?' do
subject { project.internal? }
@@ -28,213 +15,213 @@ describe "Internal Project Access", feature: true do
describe "GET /:project_path" do
subject { namespace_project_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/tree/master" do
subject { namespace_project_tree_path(project.namespace, project, project.repository.root_ref) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/commits/master" do
subject { namespace_project_commits_path(project.namespace, project, project.repository.root_ref, limit: 1) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/commit/:sha" do
subject { namespace_project_commit_path(project.namespace, project, project.repository.commit) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/compare" do
subject { namespace_project_compare_index_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/project_members" do
subject { namespace_project_project_members_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :visitor }
- it { is_expected.to be_denied_for :external }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_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')) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/edit" do
subject { edit_namespace_project_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_denied_for developer }
- it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/deploy_keys" do
subject { namespace_project_deploy_keys_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_denied_for developer }
- it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/issues" do
subject { namespace_project_issues_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/issues/:id/edit" do
let(:issue) { create(:issue, project: project) }
subject { edit_namespace_project_issue_path(project.namespace, project, issue) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/snippets" do
subject { namespace_project_snippets_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/snippets/new" do
subject { new_namespace_project_snippet_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/merge_requests" do
subject { namespace_project_merge_requests_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/merge_requests/new" do
subject { new_namespace_project_merge_request_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/branches" do
@@ -245,15 +232,15 @@ describe "Internal Project Access", feature: true do
allow_any_instance_of(Project).to receive(:branches).and_return([])
end
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/tags" do
@@ -264,58 +251,58 @@ describe "Internal Project Access", feature: true do
allow_any_instance_of(Project).to receive(:tags).and_return([])
end
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/hooks" do
subject { namespace_project_hooks_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_denied_for developer }
- it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/pipelines" do
subject { namespace_project_pipelines_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/pipelines/:id" do
let(:pipeline) { create(:ci_pipeline, project: project) }
subject { namespace_project_pipeline_path(project.namespace, project, pipeline) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/builds" do
@@ -324,29 +311,29 @@ describe "Internal Project Access", feature: true do
context "when allowed for public and internal" do
before { project.update(public_builds: true) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
context "when disallowed for public and internal" do
before { project.update(public_builds: false) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
end
@@ -358,73 +345,73 @@ describe "Internal Project Access", feature: true do
context "when allowed for public and internal" do
before { project.update(public_builds: true) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
context "when disallowed for public and internal" do
before { project.update(public_builds: false) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
end
describe "GET /:project_path/environments" do
subject { namespace_project_environments_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/environments/:id" do
let(:environment) { create(:environment, project: project) }
subject { namespace_project_environment_path(project.namespace, project, environment) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/environments/new" do
subject { new_namespace_project_environment_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/container_registry" do
@@ -435,14 +422,14 @@ describe "Internal Project Access", feature: true do
subject { namespace_project_container_registry_index_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
end
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index 79417c769a8..f52e23f9433 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -3,20 +3,7 @@ require 'spec_helper'
describe "Private Project Access", feature: true do
include AccessMatchers
- let(:project) { create(:project, :private) }
-
- let(:owner) { project.owner }
- let(:master) { create(:user) }
- let(:developer) { create(:user) }
- let(:reporter) { create(:user) }
- let(:guest) { create(:user) }
-
- before do
- project.team << [master, :master]
- project.team << [developer, :developer]
- project.team << [reporter, :reporter]
- project.team << [guest, :guest]
- end
+ let(:project) { create(:project, :private, public_builds: false) }
describe "Project should be private" do
describe '#private?' do
@@ -28,185 +15,185 @@ describe "Private Project Access", feature: true do
describe "GET /:project_path" do
subject { namespace_project_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/tree/master" do
subject { namespace_project_tree_path(project.namespace, project, project.repository.root_ref) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/commits/master" do
subject { namespace_project_commits_path(project.namespace, project, project.repository.root_ref, limit: 1) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/commit/:sha" do
subject { namespace_project_commit_path(project.namespace, project, project.repository.commit) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/compare" do
subject { namespace_project_compare_index_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/project_members" do
subject { namespace_project_project_members_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
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'))}
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/edit" do
subject { edit_namespace_project_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_denied_for developer }
- it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/deploy_keys" do
subject { namespace_project_deploy_keys_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_denied_for developer }
- it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/issues" do
subject { namespace_project_issues_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/issues/:id/edit" do
let(:issue) { create(:issue, project: project) }
subject { edit_namespace_project_issue_path(project.namespace, project, issue) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/snippets" do
subject { namespace_project_snippets_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/merge_requests" do
subject { namespace_project_merge_requests_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/branches" do
@@ -217,15 +204,15 @@ describe "Private Project Access", feature: true do
allow_any_instance_of(Project).to receive(:branches).and_return([])
end
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/tags" do
@@ -236,72 +223,108 @@ describe "Private Project Access", feature: true do
allow_any_instance_of(Project).to receive(:tags).and_return([])
end
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/hooks" do
subject { namespace_project_hooks_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_denied_for developer }
- it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/pipelines" do
subject { namespace_project_pipelines_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+
+ context 'when public builds is enabled' do
+ before do
+ project.update(public_builds: true)
+ end
+
+ it { is_expected.to be_allowed_for(:guest).of(project) }
+ end
+
+ context 'when public buils are disabled' do
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ end
end
describe "GET /:project_path/pipelines/:id" do
let(:pipeline) { create(:ci_pipeline, project: project) }
subject { namespace_project_pipeline_path(project.namespace, project, pipeline) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+
+ context 'when public builds is enabled' do
+ before do
+ project.update(public_builds: true)
+ end
+
+ it { is_expected.to be_allowed_for(:guest).of(project) }
+ end
+
+ context 'when public buils are disabled' do
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ end
end
describe "GET /:project_path/builds" do
subject { namespace_project_builds_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+
+ context 'when public builds is enabled' do
+ before do
+ project.update(public_builds: true)
+ end
+
+ it { is_expected.to be_allowed_for(:guest).of(project) }
+ end
+
+ context 'when public buils are disabled' do
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ end
end
describe "GET /:project_path/builds/:id" do
@@ -309,58 +332,75 @@ describe "Private Project Access", feature: true do
let(:build) { create(:ci_build, pipeline: pipeline) }
subject { namespace_project_build_path(project.namespace, project, build.id) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+
+ context 'when public builds is enabled' do
+ before do
+ project.update(public_builds: true)
+ end
+
+ it { is_expected.to be_allowed_for(:guest).of(project) }
+ end
+
+ context 'when public buils are disabled' do
+ before do
+ project.public_builds = false
+ project.save
+ end
+
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ end
end
describe "GET /:project_path/environments" do
subject { namespace_project_environments_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/environments/:id" do
let(:environment) { create(:environment, project: project) }
subject { namespace_project_environment_path(project.namespace, project, environment) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/environments/new" do
subject { new_namespace_project_environment_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/container_registry" do
@@ -371,14 +411,14 @@ describe "Private Project Access", feature: true do
subject { namespace_project_container_registry_index_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
end
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index 985663e7c98..bed9e92fcb6 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -5,19 +5,6 @@ describe "Public Project Access", feature: true do
let(:project) { create(:project, :public) }
- let(:owner) { project.owner }
- let(:master) { create(:user) }
- let(:developer) { create(:user) }
- let(:reporter) { create(:user) }
- let(:guest) { create(:user) }
-
- before do
- project.team << [master, :master]
- project.team << [developer, :developer]
- project.team << [reporter, :reporter]
- project.team << [guest, :guest]
- end
-
describe "Project should be public" do
describe '#public?' do
subject { project.public? }
@@ -28,114 +15,114 @@ describe "Public Project Access", feature: true do
describe "GET /:project_path" do
subject { namespace_project_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
describe "GET /:project_path/tree/master" do
subject { namespace_project_tree_path(project.namespace, project, project.repository.root_ref) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
describe "GET /:project_path/commits/master" do
subject { namespace_project_commits_path(project.namespace, project, project.repository.root_ref, limit: 1) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
describe "GET /:project_path/commit/:sha" do
subject { namespace_project_commit_path(project.namespace, project, project.repository.commit) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
describe "GET /:project_path/compare" do
subject { namespace_project_compare_index_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
describe "GET /:project_path/project_members" do
subject { namespace_project_project_members_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :visitor }
- it { is_expected.to be_allowed_for :external }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:visitor) }
+ it { is_expected.to be_allowed_for(:external) }
end
describe "GET /:project_path/pipelines" do
subject { namespace_project_pipelines_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
describe "GET /:project_path/pipelines/:id" do
let(:pipeline) { create(:ci_pipeline, project: project) }
subject { namespace_project_pipeline_path(project.namespace, project, pipeline) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
describe "GET /:project_path/builds" do
@@ -144,29 +131,29 @@ describe "Public Project Access", feature: true do
context "when allowed for public" do
before { project.update(public_builds: true) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
context "when disallowed for public" do
before { project.update(public_builds: false) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
end
@@ -178,73 +165,73 @@ describe "Public Project Access", feature: true do
context "when allowed for public" do
before { project.update(public_builds: true) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
context "when disallowed for public" do
before { project.update(public_builds: false) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
end
describe "GET /:project_path/environments" do
subject { namespace_project_environments_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/environments/:id" do
let(:environment) { create(:environment, project: project) }
subject { namespace_project_environment_path(project.namespace, project, environment) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/environments/new" do
subject { new_namespace_project_environment_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/blob" do
@@ -252,127 +239,127 @@ describe "Public Project Access", feature: true do
subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore')) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
describe "GET /:project_path/edit" do
subject { edit_namespace_project_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_denied_for developer }
- it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/deploy_keys" do
subject { namespace_project_deploy_keys_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_denied_for developer }
- it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/issues" do
subject { namespace_project_issues_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
describe "GET /:project_path/issues/:id/edit" do
let(:issue) { create(:issue, project: project) }
subject { edit_namespace_project_issue_path(project.namespace, project, issue) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/snippets" do
subject { namespace_project_snippets_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
describe "GET /:project_path/snippets/new" do
subject { new_namespace_project_snippet_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/merge_requests" do
subject { namespace_project_merge_requests_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
describe "GET /:project_path/merge_requests/new" do
subject { new_namespace_project_merge_request_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/branches" do
@@ -383,15 +370,15 @@ describe "Public Project Access", feature: true do
allow_any_instance_of(Project).to receive(:branches).and_return([])
end
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
describe "GET /:project_path/tags" do
@@ -402,29 +389,29 @@ describe "Public Project Access", feature: true do
allow_any_instance_of(Project).to receive(:tags).and_return([])
end
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
describe "GET /:project_path/hooks" do
subject { namespace_project_hooks_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_denied_for developer }
- it { is_expected.to be_denied_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/container_registry" do
@@ -435,14 +422,14 @@ describe "Public Project Access", feature: true do
subject { namespace_project_container_registry_index_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
end
diff --git a/spec/features/security/project/snippet/internal_access_spec.rb b/spec/features/security/project/snippet/internal_access_spec.rb
index 49deacc5c74..2659b3ee3ec 100644
--- a/spec/features/security/project/snippet/internal_access_spec.rb
+++ b/spec/features/security/project/snippet/internal_access_spec.rb
@@ -5,76 +5,64 @@ describe "Internal Project Snippets Access", feature: true do
let(:project) { create(:empty_project, :internal) }
- let(:owner) { project.owner }
- let(:master) { create(:user) }
- let(:developer) { create(:user) }
- let(:reporter) { create(:user) }
- let(:guest) { create(:user) }
- let(:internal_snippet) { create(:project_snippet, :internal, project: project, author: owner) }
- let(:private_snippet) { create(:project_snippet, :private, project: project, author: owner) }
-
- before do
- project.team << [master, :master]
- project.team << [developer, :developer]
- project.team << [reporter, :reporter]
- project.team << [guest, :guest]
- end
+ let(:internal_snippet) { create(:project_snippet, :internal, project: project, author: project.owner) }
+ let(:private_snippet) { create(:project_snippet, :private, project: project, author: project.owner) }
describe "GET /:project_path/snippets" do
subject { namespace_project_snippets_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/snippets/new" do
subject { new_namespace_project_snippet_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/snippets/:id" do
context "for an internal snippet" do
subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
context "for a private snippet" do
subject { namespace_project_snippet_path(project.namespace, project, private_snippet) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
end
@@ -82,29 +70,29 @@ describe "Internal Project Snippets Access", feature: true do
context "for an internal snippet" do
subject { raw_namespace_project_snippet_path(project.namespace, project, internal_snippet) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
context "for a private snippet" do
subject { raw_namespace_project_snippet_path(project.namespace, project, private_snippet) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
end
end
diff --git a/spec/features/security/project/snippet/private_access_spec.rb b/spec/features/security/project/snippet/private_access_spec.rb
index a1bfc076d99..6eb9f163bd5 100644
--- a/spec/features/security/project/snippet/private_access_spec.rb
+++ b/spec/features/security/project/snippet/private_access_spec.rb
@@ -5,73 +5,61 @@ describe "Private Project Snippets Access", feature: true do
let(:project) { create(:empty_project, :private) }
- let(:owner) { project.owner }
- let(:master) { create(:user) }
- let(:developer) { create(:user) }
- let(:reporter) { create(:user) }
- let(:guest) { create(:user) }
- let(:private_snippet) { create(:project_snippet, :private, project: project, author: owner) }
-
- before do
- project.team << [master, :master]
- project.team << [developer, :developer]
- project.team << [reporter, :reporter]
- project.team << [guest, :guest]
- end
+ let(:private_snippet) { create(:project_snippet, :private, project: project, author: project.owner) }
describe "GET /:project_path/snippets" do
subject { namespace_project_snippets_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/snippets/new" do
subject { new_namespace_project_snippet_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/snippets/:id for a private snippet" do
subject { namespace_project_snippet_path(project.namespace, project, private_snippet) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/snippets/:id/raw for a private snippet" do
subject { raw_namespace_project_snippet_path(project.namespace, project, private_snippet) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
end
diff --git a/spec/features/security/project/snippet/public_access_spec.rb b/spec/features/security/project/snippet/public_access_spec.rb
index 30bcd87ef04..f3329d0bc96 100644
--- a/spec/features/security/project/snippet/public_access_spec.rb
+++ b/spec/features/security/project/snippet/public_access_spec.rb
@@ -5,91 +5,79 @@ describe "Public Project Snippets Access", feature: true do
let(:project) { create(:empty_project, :public) }
- let(:owner) { project.owner }
- let(:master) { create(:user) }
- let(:developer) { create(:user) }
- let(:reporter) { create(:user) }
- let(:guest) { create(:user) }
- let(:public_snippet) { create(:project_snippet, :public, project: project, author: owner) }
- let(:internal_snippet) { create(:project_snippet, :internal, project: project, author: owner) }
- let(:private_snippet) { create(:project_snippet, :private, project: project, author: owner) }
-
- before do
- project.team << [master, :master]
- project.team << [developer, :developer]
- project.team << [reporter, :reporter]
- project.team << [guest, :guest]
- end
+ let(:public_snippet) { create(:project_snippet, :public, project: project, author: project.owner) }
+ let(:internal_snippet) { create(:project_snippet, :internal, project: project, author: project.owner) }
+ let(:private_snippet) { create(:project_snippet, :private, project: project, author: project.owner) }
describe "GET /:project_path/snippets" do
subject { namespace_project_snippets_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
describe "GET /:project_path/snippets/new" do
subject { new_namespace_project_snippet_path(project.namespace, project) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_denied_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/snippets/:id" do
context "for a public snippet" do
subject { namespace_project_snippet_path(project.namespace, project, public_snippet) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
context "for an internal snippet" do
subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
context "for a private snippet" do
subject { namespace_project_snippet_path(project.namespace, project, private_snippet) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
end
@@ -97,43 +85,43 @@ describe "Public Project Snippets Access", feature: true do
context "for a public snippet" do
subject { raw_namespace_project_snippet_path(project.namespace, project, public_snippet) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_allowed_for :external }
- it { is_expected.to be_allowed_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
context "for an internal snippet" do
subject { raw_namespace_project_snippet_path(project.namespace, project, internal_snippet) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
context "for a private snippet" do
subject { raw_namespace_project_snippet_path(project.namespace, project, private_snippet) }
- it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_allowed_for owner }
- it { is_expected.to be_allowed_for master }
- it { is_expected.to be_allowed_for developer }
- it { is_expected.to be_allowed_for reporter }
- it { is_expected.to be_allowed_for guest }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :external }
- it { is_expected.to be_denied_for :visitor }
+ 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_allowed_for(:developer).of(project) }
+ 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(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
end
end
diff --git a/spec/features/snippets/create_snippet_spec.rb b/spec/features/snippets/create_snippet_spec.rb
new file mode 100644
index 00000000000..cb95e7828db
--- /dev/null
+++ b/spec/features/snippets/create_snippet_spec.rb
@@ -0,0 +1,20 @@
+require 'rails_helper'
+
+feature 'Create Snippet', feature: true do
+ before do
+ login_as :user
+ visit new_snippet_path
+ end
+
+ scenario 'Authenticated user creates a snippet' do
+ fill_in 'personal_snippet_title', with: 'My Snippet Title'
+ page.within('.file-editor') do
+ find(:xpath, "//input[@id='personal_snippet_content']").set 'Hello World!'
+ end
+
+ click_button 'Create snippet'
+
+ expect(page).to have_content('My Snippet Title')
+ expect(page).to have_content('Hello World!')
+ end
+end
diff --git a/spec/features/snippets/explore_spec.rb b/spec/features/snippets/explore_spec.rb
new file mode 100644
index 00000000000..10a4597e467
--- /dev/null
+++ b/spec/features/snippets/explore_spec.rb
@@ -0,0 +1,16 @@
+require 'rails_helper'
+
+feature 'Explore Snippets', feature: true do
+ scenario 'User should see snippets that are not private' do
+ public_snippet = create(:personal_snippet, :public)
+ internal_snippet = create(:personal_snippet, :internal)
+ private_snippet = create(:personal_snippet, :private)
+
+ login_as create(:user)
+ visit explore_snippets_path
+
+ expect(page).to have_content(public_snippet.title)
+ expect(page).to have_content(internal_snippet.title)
+ expect(page).not_to have_content(private_snippet.title)
+ end
+end
diff --git a/spec/features/snippets/search_snippets_spec.rb b/spec/features/snippets/search_snippets_spec.rb
new file mode 100644
index 00000000000..146cd3af848
--- /dev/null
+++ b/spec/features/snippets/search_snippets_spec.rb
@@ -0,0 +1,66 @@
+require 'rails_helper'
+
+feature 'Search Snippets', feature: true do
+ scenario 'User searches for snippets by title' do
+ public_snippet = create(:personal_snippet, :public, title: 'Beginning and Middle')
+ private_snippet = create(:personal_snippet, :private, title: 'Middle and End')
+
+ login_as private_snippet.author
+ visit dashboard_snippets_path
+
+ page.within '.search' do
+ fill_in 'search', with: 'Middle'
+ click_button 'Go'
+ end
+
+ click_link 'Titles and Filenames'
+
+ expect(page).to have_link(public_snippet.title)
+ expect(page).to have_link(private_snippet.title)
+ end
+
+ scenario 'User searches for snippet contents' do
+ create(:personal_snippet,
+ :public,
+ title: 'Many lined snippet',
+ content: <<-CONTENT.strip_heredoc
+ |line one
+ |line two
+ |line three
+ |line four
+ |line five
+ |line six
+ |line seven
+ |line eight
+ |line nine
+ |line ten
+ |line eleven
+ |line twelve
+ |line thirteen
+ |line fourteen
+ CONTENT
+ )
+
+ login_as create(:user)
+ visit dashboard_snippets_path
+
+ page.within '.search' do
+ fill_in 'search', with: 'line seven'
+ click_button 'Go'
+ end
+
+ expect(page).to have_content('line seven')
+
+ # 3 lines before the matched line should be visible
+ expect(page).to have_content('line six')
+ expect(page).to have_content('line five')
+ expect(page).to have_content('line four')
+ expect(page).not_to have_content('line three')
+
+ # 3 lines after the matched line should be visible
+ expect(page).to have_content('line eight')
+ expect(page).to have_content('line nine')
+ expect(page).to have_content('line ten')
+ expect(page).not_to have_content('line eleven')
+ end
+end
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 3ae83ac082d..88eabea7e3a 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -44,7 +44,7 @@ describe 'Dashboard Todos', feature: true do
end
it 'shows "All done" message' do
- expect(page).to have_content("Good job! Looks like you don't have any todos left.")
+ expect(page).to have_selector('.todos-all-done', count: 1)
end
end
@@ -64,7 +64,7 @@ describe 'Dashboard Todos', feature: true do
end
it 'shows "All done" message' do
- expect(page).to have_content("Good job! Looks like you don't have any todos left.")
+ expect(page).to have_selector('.todos-all-done', count: 1)
end
end
end
@@ -152,7 +152,7 @@ describe 'Dashboard Todos', feature: true do
within('.todos-pending-count') { expect(page).to have_content '0' }
expect(page).to have_content 'To do 0'
expect(page).to have_content 'Done 0'
- expect(page).to have_content "Good job! Looks like you don't have any todos left."
+ expect(page).to have_selector('.todos-all-done', count: 1)
end
end
end
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index b750f27ea72..be21b403084 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -163,8 +163,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
click_on "Sign in via U2F device"
expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device"
-
- expect(page.body).to match('Signed in successfully')
+ expect(page.body).to match('href="/users/sign_out"')
end
end
@@ -178,7 +177,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device"
- expect(page.body).to match('Signed in successfully')
+ expect(page.body).to match('href="/users/sign_out"')
end
end
@@ -234,7 +233,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device"
- expect(page.body).to match('Signed in successfully')
+ expect(page.body).to match('href="/users/sign_out"')
end
end
end
@@ -275,7 +274,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device"
- expect(page.body).to match('Signed in successfully')
+ expect(page.body).to match('href="/users/sign_out"')
logout
end
diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb
index 33b52d1547e..e2d9cfdd0b0 100644
--- a/spec/features/unsubscribe_links_spec.rb
+++ b/spec/features/unsubscribe_links_spec.rb
@@ -26,11 +26,11 @@ describe 'Unsubscribe links', feature: true do
expect(current_path).to eq unsubscribe_sent_notification_path(SentNotification.last)
expect(page).to have_text(%(Unsubscribe from issue #{issue.title} (#{issue.to_reference})))
expect(page).to have_text(%(Are you sure you want to unsubscribe from issue #{issue.title} (#{issue.to_reference})?))
- expect(issue.subscribed?(recipient)).to be_truthy
+ expect(issue.subscribed?(recipient, project)).to be_truthy
click_link 'Unsubscribe'
- expect(issue.subscribed?(recipient)).to be_falsey
+ expect(issue.subscribed?(recipient, project)).to be_falsey
expect(current_path).to eq new_user_session_path
end
@@ -38,11 +38,11 @@ describe 'Unsubscribe links', feature: true do
visit body_link
expect(current_path).to eq unsubscribe_sent_notification_path(SentNotification.last)
- expect(issue.subscribed?(recipient)).to be_truthy
+ expect(issue.subscribed?(recipient, project)).to be_truthy
click_link 'Cancel'
- expect(issue.subscribed?(recipient)).to be_truthy
+ expect(issue.subscribed?(recipient, project)).to be_truthy
expect(current_path).to eq new_user_session_path
end
end
@@ -51,7 +51,7 @@ describe 'Unsubscribe links', feature: true do
visit header_link
expect(page).to have_text('unsubscribed')
- expect(issue.subscribed?(recipient)).to be_falsey
+ expect(issue.subscribed?(recipient, project)).to be_falsey
end
end
@@ -62,14 +62,14 @@ describe 'Unsubscribe links', feature: true do
visit body_link
expect(page).to have_text('unsubscribed')
- expect(issue.subscribed?(recipient)).to be_falsey
+ expect(issue.subscribed?(recipient, project)).to be_falsey
end
it 'unsubscribes from the issue when visiting the link from the header' do
visit header_link
expect(page).to have_text('unsubscribed')
- expect(issue.subscribed?(recipient)).to be_falsey
+ expect(issue.subscribed?(recipient, project)).to be_falsey
end
end
end
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index 111ca7f7a70..afa98f3f715 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -74,16 +74,29 @@ feature 'Users', feature: true, js: true do
visit new_user_session_path
click_link 'Register'
end
+
+ scenario 'doesn\'t show an error border if the username is available' do
+ fill_in username_input, with: 'new-user'
+ wait_for_ajax
+ expect(find('.username')).not_to have_css '.gl-field-error-outline'
+ end
+
+ scenario 'does not show an error border if the username contains dots (.)' do
+ fill_in username_input, with: 'new.user.username'
+ wait_for_ajax
+ expect(find('.username')).not_to have_css '.gl-field-error-outline'
+ end
+
scenario 'shows an error border if the username already exists' do
fill_in username_input, with: user.username
wait_for_ajax
expect(find('.username')).to have_css '.gl-field-error-outline'
end
- scenario 'doesn\'t show an error border if the username is available' do
- fill_in username_input, with: 'new-user'
+ scenario 'shows an error border if the username contains special characters' do
+ fill_in username_input, with: 'new$user!username'
wait_for_ajax
- expect(find('#new_user_username')).not_to have_css '.gl-field-error-outline'
+ expect(find('.username')).to have_css '.gl-field-error-outline'
end
end
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
index d7880d5778f..ff30ffd7820 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -29,6 +29,31 @@ describe 'Project variables', js: true do
end
end
+ it 'reveals and hides new variable' do
+ fill_in('variable_key', with: 'key')
+ fill_in('variable_value', with: 'key value')
+ click_button('Add new variable')
+
+ page.within('.variables-table') do
+ expect(page).to have_content('key')
+ expect(page).to have_content('******')
+ end
+
+ click_button('Reveal Values')
+
+ page.within('.variables-table') do
+ expect(page).to have_content('key')
+ expect(page).to have_content('key value')
+ end
+
+ click_button('Hide Values')
+
+ page.within('.variables-table') do
+ expect(page).to have_content('key')
+ expect(page).to have_content('******')
+ end
+ end
+
it 'deletes variable' do
page.within('.variables-table') do
find('.btn-variable-delete').click
diff --git a/spec/finders/access_requests_finder_spec.rb b/spec/finders/access_requests_finder_spec.rb
index 8cfea9659cb..c7278e971ae 100644
--- a/spec/finders/access_requests_finder_spec.rb
+++ b/spec/finders/access_requests_finder_spec.rb
@@ -3,12 +3,17 @@ require 'spec_helper'
describe AccessRequestsFinder, services: true do
let(:user) { create(:user) }
let(:access_requester) { create(:user) }
- let(:project) { create(:project, :public) }
- let(:group) { create(:group, :public) }
- before do
- project.request_access(access_requester)
- group.request_access(access_requester)
+ let(:project) do
+ create(:empty_project, :public, :access_requestable) do |project|
+ project.request_access(access_requester)
+ end
+ end
+
+ let(:group) do
+ create(:group, :public, :access_requestable) do |group|
+ group.request_access(access_requester)
+ end
end
shared_examples 'a finder returning access requesters' do |method_name|
diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb
index fbe09b28b3c..00eec3f3f4c 100644
--- a/spec/finders/group_projects_finder_spec.rb
+++ b/spec/finders/group_projects_finder_spec.rb
@@ -38,7 +38,10 @@ describe GroupProjectsFinder do
end
describe 'without group member current_user' do
- before { shared_project_2.team << [current_user, Gitlab::Access::MASTER] }
+ before do
+ shared_project_2.team << [current_user, Gitlab::Access::MASTER]
+ current_user.reload
+ end
context "only shared" do
context "without external user" do
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 40bccb8e50b..97737d7ddc7 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -10,22 +10,23 @@ describe IssuesFinder do
let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') }
let(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') }
let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) }
- let!(:label_link) { create(:label_link, label: label, target: issue2) }
-
- before do
- project1.team << [user, :master]
- project2.team << [user, :developer]
- project2.team << [user2, :developer]
-
- issue1
- issue2
- issue3
- end
describe '#execute' do
+ let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') }
+ let!(:label_link) { create(:label_link, label: label, target: issue2) }
let(:search_user) { user }
let(:params) { {} }
- let(:issues) { IssuesFinder.new(search_user, params.merge(scope: scope, state: 'opened')).execute }
+ let(:issues) { IssuesFinder.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
+
+ before do
+ project1.team << [user, :master]
+ project2.team << [user, :developer]
+ project2.team << [user2, :developer]
+
+ issue1
+ issue2
+ issue3
+ end
context 'scope: all' do
let(:scope) { 'all' }
@@ -143,6 +144,40 @@ describe IssuesFinder do
end
end
+ context 'filtering by state' do
+ context 'with opened' do
+ let(:params) { { state: 'opened' } }
+
+ it 'returns only opened issues' do
+ expect(issues).to contain_exactly(issue1, issue2, issue3)
+ end
+ end
+
+ context 'with closed' do
+ let(:params) { { state: 'closed' } }
+
+ it 'returns only closed issues' do
+ expect(issues).to contain_exactly(closed_issue)
+ end
+ end
+
+ context 'with all' do
+ let(:params) { { state: 'all' } }
+
+ it 'returns all issues' do
+ expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue)
+ end
+ end
+
+ context 'with invalid state' do
+ let(:params) { { state: 'invalid_state' } }
+
+ it 'returns all issues' do
+ expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue)
+ end
+ end
+ end
+
context 'when the user is unauthorized' do
let(:search_user) { nil }
@@ -158,6 +193,15 @@ describe IssuesFinder do
expect(issues).to contain_exactly(issue2, issue3)
end
end
+
+ it 'finds issues user can access due to group' do
+ group = create(:group)
+ project = create(:empty_project, group: group)
+ issue = create(:issue, project: project)
+ group.add_user(user, :owner)
+
+ expect(issues).to include(issue)
+ end
end
context 'personal scope' do
@@ -175,5 +219,43 @@ describe IssuesFinder do
end
end
end
+
+ context 'when project restricts issues' 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)
+ issue = create(:issue, project: project)
+
+ expect(issues).not_to include(issue)
+ end
+
+ it "doesn't return issues if feature disabled" do
+ [project1, project2].each do |project|
+ project.project_feature.update!(issues_access_level: ProjectFeature::DISABLED)
+ end
+
+ expect(issues.count).to eq 0
+ end
+ end
+ end
+
+ describe '.not_restricted_by_confidentiality' do
+ let(:authorized_user) { create(:user) }
+ let(:project) { create(:empty_project, namespace: authorized_user.namespace) }
+ let!(:public_issue) { create(:issue, project: project) }
+ let!(:confidential_issue) { create(:issue, project: project, confidential: true) }
+
+ it 'returns non confidential issues for nil user' do
+ expect(IssuesFinder.send(:not_restricted_by_confidentiality, nil)).to include(public_issue)
+ end
+
+ it 'returns non confidential issues for user not authorized for the issues projects' do
+ expect(IssuesFinder.send(:not_restricted_by_confidentiality, user)).to include(public_issue)
+ end
+
+ it 'returns all issues for user authorized for the issues projects' do
+ expect(IssuesFinder.send(:not_restricted_by_confidentiality, authorized_user)).to include(public_issue, confidential_issue)
+ end
end
end
diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb
index 10cfb66ec1c..1724cdba830 100644
--- a/spec/finders/labels_finder_spec.rb
+++ b/spec/finders/labels_finder_spec.rb
@@ -17,7 +17,7 @@ describe LabelsFinder do
let!(:project_label_4) { create(:label, project: project_4, title: 'Label 4') }
let!(:project_label_5) { create(:label, project: project_5, title: 'Label 5') }
- let!(:group_label_1) { create(:group_label, group: group_1, title: 'Label 1') }
+ let!(:group_label_1) { create(:group_label, group: group_1, title: 'Label 1 (group)') }
let!(:group_label_2) { create(:group_label, group: group_1, title: 'Group Label 2') }
let!(:group_label_3) { create(:group_label, group: group_2, title: 'Group Label 3') }
@@ -64,6 +64,21 @@ describe LabelsFinder do
expect(finder.execute).to eq [group_label_2, project_label_1, group_label_1]
end
+
+ context 'as an administrator' do
+ it 'does not return labels from another project' do
+ # Purposefully creating a project with _nothing_ associated to it
+ isolated_project = create(:empty_project)
+ admin = create(:admin)
+
+ # project_3 has a label associated to it, which we don't want coming
+ # back when we ask for the isolated project's labels
+ project_3.team << [admin, :reporter]
+ finder = described_class.new(admin, project_id: isolated_project.id)
+
+ expect(finder.execute).to be_empty
+ end
+ end
end
context 'filtering by title' do
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 535aabfc18d..88361e27102 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -6,14 +6,17 @@ describe MergeRequestsFinder do
let(:project1) { create(:project) }
let(:project2) { create(:project, forked_from_project: project1) }
+ let(:project3) { create(:project, forked_from_project: project1, archived: true) }
let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) }
let!(:merge_request2) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1, state: 'closed') }
let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2) }
+ let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3) }
before do
project1.team << [user, :master]
project2.team << [user, :developer]
+ project3.team << [user, :developer]
project2.team << [user2, :developer]
end
@@ -21,7 +24,7 @@ describe MergeRequestsFinder do
it 'filters by scope' do
params = { scope: 'authored', state: 'opened' }
merge_requests = MergeRequestsFinder.new(user, params).execute
- expect(merge_requests.size).to eq(2)
+ expect(merge_requests.size).to eq(3)
end
it 'filters by project' do
@@ -29,5 +32,11 @@ describe MergeRequestsFinder do
merge_requests = MergeRequestsFinder.new(user, params).execute
expect(merge_requests.size).to eq(1)
end
+
+ it 'filters by non_archived' do
+ params = { non_archived: true }
+ merge_requests = MergeRequestsFinder.new(user, params).execute
+ expect(merge_requests.size).to eq(3)
+ end
end
end
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index 7c6860372cc..4d21254323c 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -2,59 +2,203 @@ require 'spec_helper'
describe NotesFinder do
let(:user) { create :user }
- let(:project) { create :project }
- let(:note1) { create :note_on_commit, project: project }
- let(:note2) { create :note_on_commit, project: project }
- let(:commit) { note1.noteable }
+ let(:project) { create(:empty_project) }
before do
project.team << [user, :master]
end
describe '#execute' do
- let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } }
+ it 'finds notes on snippets when project is public and user isnt a member'
- before do
- note1
- note2
+ it 'finds notes on merge requests' do
+ create(:note_on_merge_request, project: project)
+
+ notes = described_class.new(project, user).execute
+
+ expect(notes.count).to eq(1)
end
- it 'finds all notes' do
- notes = NotesFinder.new.execute(project, user, params)
- expect(notes.size).to eq(2)
+ it 'finds notes on snippets' do
+ create(:note_on_project_snippet, project: project)
+
+ notes = described_class.new(project, user).execute
+
+ expect(notes.count).to eq(1)
end
- it 'raises an exception for an invalid target_type' do
- params.merge!(target_type: 'invalid')
- expect { NotesFinder.new.execute(project, user, params) }.to raise_error('invalid target_type')
+ it "excludes notes on commits the author can't download" do
+ project = create(:project, :private)
+ note = create(:note_on_commit, project: project)
+ params = { target_type: 'commit', target_id: note.noteable.id }
+
+ notes = described_class.new(project, create(:user), params).execute
+
+ expect(notes.count).to eq(0)
end
- it 'filters out old notes' do
- note2.update_attribute(:updated_at, 2.hours.ago)
- notes = NotesFinder.new.execute(project, user, params)
- expect(notes).to eq([note1])
+ it 'succeeds when no notes found' do
+ notes = described_class.new(project, create(:user)).execute
+
+ expect(notes.count).to eq(0)
end
- context 'confidential issue notes' do
- let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
- let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) }
+ 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)
+ end
+
+ it 'publicly excludes notes on merge requests' do
+ create(:note_on_merge_request, project: project)
+
+ notes = described_class.new(project, create(:user)).execute
+
+ expect(notes.count).to eq(0)
+ end
+
+ it 'publicly excludes notes on issues' do
+ create(:note_on_issue, project: project)
+
+ notes = described_class.new(project, create(:user)).execute
+
+ expect(notes.count).to eq(0)
+ end
+
+ it 'publicly excludes notes on snippets' do
+ create(:note_on_project_snippet, project: project)
+
+ notes = described_class.new(project, create(:user)).execute
+
+ expect(notes.count).to eq(0)
+ end
+ end
+
+ context 'for target' do
+ let(:project) { create(:project) }
+ let(:note1) { create :note_on_commit, project: project }
+ let(:note2) { create :note_on_commit, project: project }
+ let(:commit) { note1.noteable }
+ let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } }
+
+ before do
+ note1
+ note2
+ end
+
+ it 'finds all notes' do
+ notes = described_class.new(project, user, params).execute
+ expect(notes.size).to eq(2)
+ end
+
+ it 'finds notes on merge requests' do
+ note = create(:note_on_merge_request, project: project)
+ params = { target_type: 'merge_request', target_id: note.noteable.id }
+
+ notes = described_class.new(project, user, params).execute
+
+ expect(notes).to include(note)
+ end
+
+ it 'finds notes on snippets' do
+ note = create(:note_on_project_snippet, project: project)
+ params = { target_type: 'snippet', target_id: note.noteable.id }
- let(:params) { { target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } }
+ notes = described_class.new(project, user, params).execute
- it 'returns notes if user can see the issue' do
- expect(NotesFinder.new.execute(project, user, params)).to eq([confidential_note])
+ expect(notes.count).to eq(1)
end
- it 'raises an error if user can not see the issue' do
+ it 'raises an exception for an invalid target_type' do
+ params.merge!(target_type: 'invalid')
+ expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type')
+ end
+
+ it 'filters out old notes' do
+ note2.update_attribute(:updated_at, 2.hours.ago)
+ notes = described_class.new(project, user, params).execute
+ expect(notes).to eq([note1])
+ end
+
+ context 'confidential issue notes' do
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
+ let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) }
+
+ let(:params) { { target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } }
+
+ it 'returns notes if user can see the issue' do
+ expect(described_class.new(project, user, params).execute).to eq([confidential_note])
+ end
+
+ it 'raises an error if user can not see the issue' do
+ user = create(:user)
+ expect { described_class.new(project, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'raises an error for project members with guest role' do
+ user = create(:user)
+ project.team << [user, :guest]
+
+ expect { described_class.new(project, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+ end
+
+ describe '.search' do
+ let(:project) { create(:empty_project, :public) }
+ let(:note) { create(:note_on_issue, note: 'WoW', project: project) }
+
+ it 'returns notes with matching content' do
+ expect(described_class.new(note.project, nil, search: note.note).execute).to eq([note])
+ end
+
+ it 'returns notes with matching content regardless of the casing' do
+ expect(described_class.new(note.project, nil, search: 'WOW').execute).to eq([note])
+ end
+
+ it 'returns commit notes user can access' do
+ note = create(:note_on_commit, project: project)
+
+ expect(described_class.new(note.project, create(:user), search: note.note).execute).to eq([note])
+ end
+
+ context "confidential issues" do
+ let(:user) { create(:user) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
+ let(:confidential_note) { create(:note, note: "Random", noteable: confidential_issue, project: confidential_issue.project) }
+
+ it "returns notes with matching content if user can see the issue" do
+ expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to eq([confidential_note])
+ end
+
+ it "does not return notes with matching content if user can not see the issue" do
user = create(:user)
- expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound)
+ expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to be_empty
end
- it 'raises an error for project members with guest role' do
+ it "does not return notes with matching content for project members with guest role" do
user = create(:user)
project.team << [user, :guest]
+ expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to be_empty
+ end
+
+ it "does not return notes with matching content for unauthenticated users" do
+ expect(described_class.new(confidential_note.project, nil, search: confidential_note.note).execute).to be_empty
+ end
+ end
+
+ context 'inlines SQL filters on subqueries for performance' do
+ let(:sql) { described_class.new(note.project, nil, search: note.note).execute.to_sql }
+ let(:number_of_noteable_types) { 4 }
+
+ specify 'project_id check' do
+ expect(sql.scan(/project_id/).count).to be >= (number_of_noteable_types + 2)
+ end
- expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound)
+ specify 'search filter' do
+ expect(sql.scan(/LIKE/).count).to be >= number_of_noteable_types
end
end
end
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index 28bdc18e840..975e99c5807 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -9,65 +9,74 @@ describe SnippetsFinder do
let(:project2) { create(:empty_project, :private, group: group) }
context ':all filter' do
- before do
- @snippet1 = create(:personal_snippet, :private)
- @snippet2 = create(:personal_snippet, :internal)
- @snippet3 = create(:personal_snippet, :public)
- end
+ let!(:snippet1) { create(:personal_snippet, :private) }
+ let!(:snippet2) { create(:personal_snippet, :internal) }
+ let!(:snippet3) { create(:personal_snippet, :public) }
it "returns all private and internal snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :all)
- expect(snippets).to include(@snippet2, @snippet3)
- expect(snippets).not_to include(@snippet1)
+ expect(snippets).to include(snippet2, snippet3)
+ expect(snippets).not_to include(snippet1)
end
it "returns all public snippets" do
snippets = SnippetsFinder.new.execute(nil, filter: :all)
- expect(snippets).to include(@snippet3)
- expect(snippets).not_to include(@snippet1, @snippet2)
+ expect(snippets).to include(snippet3)
+ expect(snippets).not_to include(snippet1, snippet2)
end
end
- context ':by_user filter' do
- before do
- @snippet1 = create(:personal_snippet, :private, author: user)
- @snippet2 = create(:personal_snippet, :internal, author: user)
- @snippet3 = create(:personal_snippet, :public, author: user)
+ context ':public filter' do
+ let!(:snippet1) { create(:personal_snippet, :private) }
+ let!(:snippet2) { create(:personal_snippet, :internal) }
+ let!(:snippet3) { create(:personal_snippet, :public) }
+
+ it "returns public public snippets" do
+ snippets = SnippetsFinder.new.execute(nil, filter: :public)
+
+ expect(snippets).to include(snippet3)
+ expect(snippets).not_to include(snippet1, snippet2)
end
+ end
+
+ context ':by_user filter' do
+ let!(:snippet1) { create(:personal_snippet, :private, author: user) }
+ let!(:snippet2) { create(:personal_snippet, :internal, author: user) }
+ let!(:snippet3) { create(:personal_snippet, :public, author: user) }
it "returns all public and internal snippets" do
snippets = SnippetsFinder.new.execute(user1, filter: :by_user, user: user)
- expect(snippets).to include(@snippet2, @snippet3)
- expect(snippets).not_to include(@snippet1)
+ expect(snippets).to include(snippet2, snippet3)
+ expect(snippets).not_to include(snippet1)
end
it "returns internal snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_internal")
- expect(snippets).to include(@snippet2)
- expect(snippets).not_to include(@snippet1, @snippet3)
+ expect(snippets).to include(snippet2)
+ expect(snippets).not_to include(snippet1, snippet3)
end
it "returns private snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_private")
- expect(snippets).to include(@snippet1)
- expect(snippets).not_to include(@snippet2, @snippet3)
+ expect(snippets).to include(snippet1)
+ expect(snippets).not_to include(snippet2, snippet3)
end
it "returns public snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_public")
- expect(snippets).to include(@snippet3)
- expect(snippets).not_to include(@snippet1, @snippet2)
+ expect(snippets).to include(snippet3)
+ expect(snippets).not_to include(snippet1, snippet2)
end
it "returns all snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user)
- expect(snippets).to include(@snippet1, @snippet2, @snippet3)
+ expect(snippets).to include(snippet1, snippet2, snippet3)
end
it "returns only public snippets if unauthenticated user" do
snippets = SnippetsFinder.new.execute(nil, filter: :by_user, user: user)
- expect(snippets).to include(@snippet3)
- expect(snippets).not_to include(@snippet2, @snippet1)
+ expect(snippets).to include(snippet3)
+ expect(snippets).not_to include(snippet2, snippet1)
end
end
@@ -84,16 +93,39 @@ describe SnippetsFinder do
expect(snippets).not_to include(@snippet1, @snippet2)
end
- it "returns public and internal snippets for none project members" do
+ it "returns public and internal snippets for non project members" do
snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1)
expect(snippets).to include(@snippet2, @snippet3)
expect(snippets).not_to include(@snippet1)
end
+ it "returns public snippets for non project members" do
+ snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_public")
+ expect(snippets).to include(@snippet3)
+ expect(snippets).not_to include(@snippet1, @snippet2)
+ end
+
+ it "returns internal snippets for non project members" do
+ snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_internal")
+ expect(snippets).to include(@snippet2)
+ expect(snippets).not_to include(@snippet1, @snippet3)
+ end
+
+ it "does not return private snippets for non project members" do
+ snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private")
+ expect(snippets).not_to include(@snippet1, @snippet2, @snippet3)
+ end
+
it "returns all snippets for project members" do
project1.team << [user, :developer]
snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1)
expect(snippets).to include(@snippet1, @snippet2, @snippet3)
end
+
+ it "returns private snippets for project members" do
+ project1.team << [user, :developer]
+ snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private")
+ expect(snippets).to include(@snippet1)
+ end
end
end
diff --git a/spec/fixtures/api/schemas/user/login.json b/spec/fixtures/api/schemas/user/login.json
new file mode 100644
index 00000000000..e6c1d9c9d84
--- /dev/null
+++ b/spec/fixtures/api/schemas/user/login.json
@@ -0,0 +1,37 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "username",
+ "email",
+ "name",
+ "state",
+ "avatar_url",
+ "web_url",
+ "created_at",
+ "is_admin",
+ "bio",
+ "location",
+ "skype",
+ "linkedin",
+ "twitter",
+ "website_url",
+ "organization",
+ "last_sign_in_at",
+ "confirmed_at",
+ "theme_id",
+ "color_scheme_id",
+ "projects_limit",
+ "current_sign_in_at",
+ "identities",
+ "can_create_group",
+ "can_create_project",
+ "two_factor_enabled",
+ "external",
+ "private_token"
+ ],
+ "properties": {
+ "$ref": "full.json",
+ "private_token": { "type": "string" }
+ }
+}
diff --git a/spec/fixtures/api/schemas/user/public.json b/spec/fixtures/api/schemas/user/public.json
new file mode 100644
index 00000000000..dbd5d32e89c
--- /dev/null
+++ b/spec/fixtures/api/schemas/user/public.json
@@ -0,0 +1,79 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "username",
+ "email",
+ "name",
+ "state",
+ "avatar_url",
+ "web_url",
+ "created_at",
+ "is_admin",
+ "bio",
+ "location",
+ "skype",
+ "linkedin",
+ "twitter",
+ "website_url",
+ "organization",
+ "last_sign_in_at",
+ "confirmed_at",
+ "theme_id",
+ "color_scheme_id",
+ "projects_limit",
+ "current_sign_in_at",
+ "identities",
+ "can_create_group",
+ "can_create_project",
+ "two_factor_enabled",
+ "external"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "username": { "type": "string" },
+ "email": {
+ "type": "string",
+ "pattern": "^[^@]+@[^@]+$"
+ },
+ "name": { "type": "string" },
+ "state": {
+ "type": "string",
+ "enum": ["active", "blocked"]
+ },
+ "avatar_url": { "type": "string" },
+ "web_url": { "type": "string" },
+ "created_at": { "type": "date" },
+ "is_admin": { "type": "boolean" },
+ "bio": { "type": ["string", "null"] },
+ "location": { "type": ["string", "null"] },
+ "skype": { "type": "string" },
+ "linkedin": { "type": "string" },
+ "twitter": { "type": "string "},
+ "website_url": { "type": "string" },
+ "organization": { "type": ["string", "null"] },
+ "last_sign_in_at": { "type": "date" },
+ "confirmed_at": { "type": ["date", "null"] },
+ "theme_id": { "type": "integer" },
+ "color_scheme_id": { "type": "integer" },
+ "projects_limit": { "type": "integer" },
+ "current_sign_in_at": { "type": "date" },
+ "identities": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "provider": {
+ "type": "string",
+ "enum": ["github", "bitbucket", "google_oauth2"]
+ },
+ "extern_uid": { "type": ["number", "string"] }
+ }
+ }
+ },
+ "can_create_group": { "type": "boolean" },
+ "can_create_project": { "type": "boolean" },
+ "two_factor_enabled": { "type": "boolean" },
+ "external": { "type": "boolean" }
+ }
+}
diff --git a/spec/fixtures/emails/outlook_html.eml b/spec/fixtures/emails/outlook_html.eml
new file mode 100644
index 00000000000..506d69efe83
--- /dev/null
+++ b/spec/fixtures/emails/outlook_html.eml
@@ -0,0 +1,140 @@
+
+MIME-Version: 1.0
+Received: by 10.25.161.144 with HTTP; Tue, 7 Oct 2014 22:17:17 -0700 (PDT)
+X-Originating-IP: [117.207.85.84]
+In-Reply-To: <5434c8b52bb3a_623ff09fec70f049749@discourse-app.mail>
+References: <topic/35@discourse.techapj.com>
+ <5434c8b52bb3a_623ff09fec70f049749@discourse-app.mail>
+Date: Wed, 8 Oct 2014 10:47:17 +0530
+Delivered-To: arpit@techapj.com
+Message-ID: <CAOJeqne=SJ_LwN4sb-0Y95ejc2OpreVhdmcPn0TnmwSvTCYzzQ@mail.gmail.com>
+Subject: Re: [Discourse] [Meta] Welcome to techAPJ's Discourse!
+From: Arpit Jalan <arpit@techapj.com>
+To: Discourse <mail+e1c7f2a380e33840aeb654f075490bad@arpitjalan.com>Accept-Language: en-US
+Content-Language: en-US
+X-MS-Has-Attach:
+X-MS-TNEF-Correlator:
+x-originating-ip: [134.68.31.227]
+Content-Type: multipart/alternative;
+ boundary="_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_"
+MIME-Version: 1.0
+
+--_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_
+Content-Type: text/html; charset="utf-8"
+Content-Transfer-Encoding: base64
+
+PGh0bWwgeG1sbnM6dj0idXJuOnNjaGVtYXMtbWljcm9zb2Z0LWNvbTp2bWwiIHhtbG5zOm89InVy
+bjpzY2hlbWFzLW1pY3Jvc29mdC1jb206b2ZmaWNlOm9mZmljZSIgeG1sbnM6dz0idXJuOnNjaGVt
+YXMtbWljcm9zb2Z0LWNvbTpvZmZpY2U6d29yZCIgeG1sbnM6bT0iaHR0cDovL3NjaGVtYXMubWlj
+cm9zb2Z0LmNvbS9vZmZpY2UvMjAwNC8xMi9vbW1sIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcv
+VFIvUkVDLWh0bWw0MCI+DQo8aGVhZD4NCjxtZXRhIGh0dHAtZXF1aXY9IkNvbnRlbnQtVHlwZSIg
+Y29udGVudD0idGV4dC9odG1sOyBjaGFyc2V0PXV0Zi04Ij4NCjxtZXRhIG5hbWU9IkdlbmVyYXRv
+ciIgY29udGVudD0iTWljcm9zb2Z0IFdvcmQgMTQgKGZpbHRlcmVkIG1lZGl1bSkiPg0KPCEtLVtp
+ZiAhbXNvXT48c3R5bGU+dlw6KiB7YmVoYXZpb3I6dXJsKCNkZWZhdWx0I1ZNTCk7fQ0Kb1w6KiB7
+YmVoYXZpb3I6dXJsKCNkZWZhdWx0I1ZNTCk7fQ0Kd1w6KiB7YmVoYXZpb3I6dXJsKCNkZWZhdWx0
+I1ZNTCk7fQ0KLnNoYXBlIHtiZWhhdmlvcjp1cmwoI2RlZmF1bHQjVk1MKTt9DQo8L3N0eWxlPjwh
+W2VuZGlmXS0tPjxzdHlsZT48IS0tDQovKiBGb250IERlZmluaXRpb25zICovDQpAZm9udC1mYWNl
+DQoJe2ZvbnQtZmFtaWx5OkNhbGlicmk7DQoJcGFub3NlLTE6MiAxNSA1IDIgMiAyIDQgMyAyIDQ7
+fQ0KQGZvbnQtZmFjZQ0KCXtmb250LWZhbWlseTpUYWhvbWE7DQoJcGFub3NlLTE6MiAxMSA2IDQg
+MyA1IDQgNCAyIDQ7fQ0KLyogU3R5bGUgRGVmaW5pdGlvbnMgKi8NCnAuTXNvTm9ybWFsLCBsaS5N
+c29Ob3JtYWwsIGRpdi5Nc29Ob3JtYWwNCgl7bWFyZ2luOjBpbjsNCgltYXJnaW4tYm90dG9tOi4w
+MDAxcHQ7DQoJZm9udC1zaXplOjEyLjBwdDsNCglmb250LWZhbWlseToiVGltZXMgTmV3IFJvbWFu
+Iiwic2VyaWYiO30NCmE6bGluaywgc3Bhbi5Nc29IeXBlcmxpbmsNCgl7bXNvLXN0eWxlLXByaW9y
+aXR5Ojk5Ow0KCWNvbG9yOmJsdWU7DQoJdGV4dC1kZWNvcmF0aW9uOnVuZGVybGluZTt9DQphOnZp
+c2l0ZWQsIHNwYW4uTXNvSHlwZXJsaW5rRm9sbG93ZWQNCgl7bXNvLXN0eWxlLXByaW9yaXR5Ojk5
+Ow0KCWNvbG9yOnB1cnBsZTsNCgl0ZXh0LWRlY29yYXRpb246dW5kZXJsaW5lO30NCnANCgl7bXNv
+LXN0eWxlLXByaW9yaXR5Ojk5Ow0KCW1zby1tYXJnaW4tdG9wLWFsdDphdXRvOw0KCW1hcmdpbi1y
+aWdodDowaW47DQoJbXNvLW1hcmdpbi1ib3R0b20tYWx0OmF1dG87DQoJbWFyZ2luLWxlZnQ6MGlu
+Ow0KCWZvbnQtc2l6ZToxMi4wcHQ7DQoJZm9udC1mYW1pbHk6IlRpbWVzIE5ldyBSb21hbiIsInNl
+cmlmIjt9DQpzcGFuLkVtYWlsU3R5bGUxOA0KCXttc28tc3R5bGUtdHlwZTpwZXJzb25hbC1yZXBs
+eTsNCglmb250LWZhbWlseToiQ2FsaWJyaSIsInNhbnMtc2VyaWYiOw0KCWNvbG9yOiMxRjQ5N0Q7
+fQ0KLk1zb0NocERlZmF1bHQNCgl7bXNvLXN0eWxlLXR5cGU6ZXhwb3J0LW9ubHk7DQoJZm9udC1m
+YW1pbHk6IkNhbGlicmkiLCJzYW5zLXNlcmlmIjt9DQpAcGFnZSBXb3JkU2VjdGlvbjENCgl7c2l6
+ZTo4LjVpbiAxMS4waW47DQoJbWFyZ2luOjEuMGluIDEuMGluIDEuMGluIDEuMGluO30NCmRpdi5X
+b3JkU2VjdGlvbjENCgl7cGFnZTpXb3JkU2VjdGlvbjE7fQ0KLS0+PC9zdHlsZT48IS0tW2lmIGd0
+ZSBtc28gOV0+PHhtbD4NCjxvOnNoYXBlZGVmYXVsdHMgdjpleHQ9ImVkaXQiIHNwaWRtYXg9IjEw
+MjYiIC8+DQo8L3htbD48IVtlbmRpZl0tLT48IS0tW2lmIGd0ZSBtc28gOV0+PHhtbD4NCjxvOnNo
+YXBlbGF5b3V0IHY6ZXh0PSJlZGl0Ij4NCjxvOmlkbWFwIHY6ZXh0PSJlZGl0IiBkYXRhPSIxIiAv
+Pg0KPC9vOnNoYXBlbGF5b3V0PjwveG1sPjwhW2VuZGlmXS0tPg0KPC9oZWFkPg0KPGJvZHkgbGFu
+Zz0iRU4tVVMiIGxpbms9ImJsdWUiIHZsaW5rPSJwdXJwbGUiPg0KPGRpdiBjbGFzcz0iV29yZFNl
+Y3Rpb24xIj4NCjxwIGNsYXNzPSJNc29Ob3JtYWwiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTEu
+MHB0O2ZvbnQtZmFtaWx5OiZxdW90O0NhbGlicmkmcXVvdDssJnF1b3Q7c2Fucy1zZXJpZiZxdW90
+Oztjb2xvcjojMUY0OTdEIj5NaWNyb3NvZnQgT3V0bG9vayAyMDEwPG86cD48L286cD48L3NwYW4+
+PC9wPg0KPHAgY2xhc3M9Ik1zb05vcm1hbCI+PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxMS4wcHQ7
+Zm9udC1mYW1pbHk6JnF1b3Q7Q2FsaWJyaSZxdW90OywmcXVvdDtzYW5zLXNlcmlmJnF1b3Q7O2Nv
+bG9yOiMxRjQ5N0QiPjxvOnA+Jm5ic3A7PC9vOnA+PC9zcGFuPjwvcD4NCjxwIGNsYXNzPSJNc29O
+b3JtYWwiPjxiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTAuMHB0O2ZvbnQtZmFtaWx5OiZxdW90
+O1RhaG9tYSZxdW90OywmcXVvdDtzYW5zLXNlcmlmJnF1b3Q7Ij5Gcm9tOjwvc3Bhbj48L2I+PHNw
+YW4gc3R5bGU9ImZvbnQtc2l6ZToxMC4wcHQ7Zm9udC1mYW1pbHk6JnF1b3Q7VGFob21hJnF1b3Q7
+LCZxdW90O3NhbnMtc2VyaWYmcXVvdDsiPiBtaWNoYWVsIFttYWlsdG86dGFsa0BvcGVubXJzLm9y
+Z10NCjxicj4NCjxiPlNlbnQ6PC9iPiBNb25kYXksIE9jdG9iZXIgMTMsIDIwMTQgOTozOCBBTTxi
+cj4NCjxiPlRvOjwvYj4gUG93ZXIsIENocmlzPGJyPg0KPGI+U3ViamVjdDo8L2I+IFtQTV0gWW91
+ciBwb3N0IGluICZxdW90O0J1cmdlcmhhdXM6IE5ldyByZXN0YXVyYW50IC8gbHVuY2ggdmVudWUm
+cXVvdDs8bzpwPjwvbzpwPjwvc3Bhbj48L3A+DQo8cCBjbGFzcz0iTXNvTm9ybWFsIj48bzpwPiZu
+YnNwOzwvbzpwPjwvcD4NCjxkaXY+DQo8dGFibGUgY2xhc3M9Ik1zb05vcm1hbFRhYmxlIiBib3Jk
+ZXI9IjAiIGNlbGxzcGFjaW5nPSIwIiBjZWxscGFkZGluZz0iMCI+DQo8dGJvZHk+DQo8dHI+DQo8
+dGQgdmFsaWduPSJ0b3AiIHN0eWxlPSJwYWRkaW5nOjBpbiAwaW4gMGluIDBpbiI+PC90ZD4NCjx0
+ZCBzdHlsZT0icGFkZGluZzowaW4gMGluIDBpbiAwaW4iPg0KPHAgY2xhc3M9Ik1zb05vcm1hbCIg
+c3R5bGU9Im1hcmdpbi1ib3R0b206MTguNzVwdCI+PGEgaHJlZj0iaHR0cDovL2NsLm9wZW5tcnMu
+b3JnL3RyYWNrL2NsaWNrLzMwMDM5OTA1L3RhbGsub3Blbm1ycy5vcmc/cD1leUp6SWpvaWJHbFph
+MVYwZVhoQ1kwMU1SVEZzVURKbVl6VlFNMFpsZWpFNElpd2lkaUk2TVN3aWNDSTZJbnRjSW5WY0lq
+b3pNREF6T1Rrd05TeGNJblpjSWpveExGd2lkWEpzWENJNlhDSm9kSFJ3Y3pwY1hGd3ZYRnhjTDNS
+aGJHc3ViM0JsYm0xeWN5NXZjbWRjWEZ3dmRYTmxjbk5jWEZ3dmJXbGphR0ZsYkZ3aUxGd2lhV1Jj
+SWpwY0ltUTFZbU13TjJOa05EUmpaRFE0TUdNNFlUZzJNemxqWldJMU56Z3pZbVkyWENJc1hDSjFj
+bXhmYVdSelhDSTZXMXdpWWpoa09EZzFNams1TnpkbVpqWTFaV1l5TlRFM09XUmlOR1l5TVdJM056
+RmpOemhqWmpoa09Gd2lYWDBpZlEiIHRhcmdldD0iX2JsYW5rIj48Yj48c3BhbiBzdHlsZT0iZm9u
+dC1zaXplOjEwLjBwdDtmb250LWZhbWlseTomcXVvdDtUYWhvbWEmcXVvdDssJnF1b3Q7c2Fucy1z
+ZXJpZiZxdW90Oztjb2xvcjojMDA2Njk5O3RleHQtZGVjb3JhdGlvbjpub25lIj5taWNoYWVsPC9z
+cGFuPjwvYj48L2E+PGJyPg0KPHNwYW4gc3R5bGU9ImZvbnQtc2l6ZTo4LjVwdDtmb250LWZhbWls
+eTomcXVvdDtUYWhvbWEmcXVvdDssJnF1b3Q7c2Fucy1zZXJpZiZxdW90Oztjb2xvcjojOTk5OTk5
+Ij5PY3RvYmVyIDEzPC9zcGFuPg0KPG86cD48L286cD48L3A+DQo8L3RkPg0KPC90cj4NCjx0cj4N
+Cjx0ZCBjb2xzcGFuPSIyIiBzdHlsZT0icGFkZGluZzozLjc1cHQgMGluIDBpbiAwaW4iPg0KPHAg
+Y2xhc3M9Ik1zb05vcm1hbCIgc3R5bGU9Im1hcmdpbi1ib3R0b206MTguNzVwdCI+PGEgaHJlZj0i
+aHR0cDovL2NsLm9wZW5tcnMub3JnL3RyYWNrL2NsaWNrLzMwMDM5OTA1L3RhbGsub3Blbm1ycy5v
+cmc/cD1leUp6SWpvaVVFUklTVU55UjNsVk1EZEJWVmhwV25SM1dXeDRNV05zVFc1Wklpd2lkaUk2
+TVN3aWNDSTZJbnRjSW5WY0lqb3pNREF6T1Rrd05TeGNJblpjSWpveExGd2lkWEpzWENJNlhDSm9k
+SFJ3Y3pwY1hGd3ZYRnhjTDNSaGJHc3ViM0JsYm0xeWN5NXZjbWRjWEZ3dmRGeGNYQzlpZFhKblpY
+Sm9ZWFZ6TFc1bGR5MXlaWE4wWVhWeVlXNTBMV3gxYm1Ob0xYWmxiblZsWEZ4Y0x6WTNNbHhjWEM4
+elhDSXNYQ0pwWkZ3aU9sd2laRFZpWXpBM1kyUTBOR05rTkRnd1l6aGhPRFl6T1dObFlqVTNPRE5p
+WmpaY0lpeGNJblZ5YkY5cFpITmNJanBiWENKaU56WmlZamswWlRGaU56STVaVGsyWlRSbFpXTTRO
+R1JtTWpRNE1ETXdZall5WVdZeU1HTTBYQ0pkZlNKOSI+PGI+PHNwYW4gc3R5bGU9ImNvbG9yOiMw
+MDY2OTk7dGV4dC1kZWNvcmF0aW9uOm5vbmUiPmh0dHBzOi8vdGFsay5vcGVubXJzLm9yZy90L2J1
+cmdlcmhhdXMtbmV3LXJlc3RhdXJhbnQtbHVuY2gtdmVudWUvNjcyLzM8L3NwYW4+PC9iPjwvYT4N
+CjxvOnA+PC9vOnA+PC9wPg0KPHAgc3R5bGU9Im1hcmdpbi10b3A6MGluIj5Mb29rcyBsaWtlIHlv
+dXIgcmVwbHktYnktZW1haWwgd2Fzbid0IHByb2Nlc3NlZCBjb3JyZWN0bHkgYnkgb3VyIHNvZnR3
+YXJlLiBDYW4geW91IGxldCBtZSBrbm93IHdoYXQgdmVyc2lvbi9PUyBvZiB3aGF0IGVtYWlsIHBy
+b2dyYW0geW91J3JlIHVzaW5nPyBXZSB3aWxsIHdhbnQgdG8gdHJ5IHRvIGZpeCB0aGUgYnVnLiA6
+c21pbGU6PG86cD48L286cD48L3A+DQo8cCBzdHlsZT0ibWFyZ2luLXRvcDowaW4iPlRoYW5rcyE8
+bzpwPjwvbzpwPjwvcD4NCjwvdGQ+DQo8L3RyPg0KPC90Ym9keT4NCjwvdGFibGU+DQo8ZGl2IGNs
+YXNzPSJNc29Ob3JtYWwiIGFsaWduPSJjZW50ZXIiIHN0eWxlPSJ0ZXh0LWFsaWduOmNlbnRlciI+
+DQo8aHIgc2l6ZT0iMSIgd2lkdGg9IjEwMCUiIGFsaWduPSJjZW50ZXIiPg0KPC9kaXY+DQo8ZGl2
+Pg0KPHA+PHNwYW4gc3R5bGU9ImNvbG9yOiM2NjY2NjYiPlRvIHJlc3BvbmQsIHJlcGx5IHRvIHRo
+aXMgZW1haWwgb3IgdmlzaXQgPGEgaHJlZj0iaHR0cDovL2NsLm9wZW5tcnMub3JnL3RyYWNrL2Ns
+aWNrLzMwMDM5OTA1L3RhbGsub3Blbm1ycy5vcmc/cD1leUp6SWpvaWVYaDJWbnBGTUhSMU1uRm5a
+RWR1TlhFd01GcFFPVlp0VFZvNElpd2lkaUk2TVN3aWNDSTZJbnRjSW5WY0lqb3pNREF6T1Rrd05T
+eGNJblpjSWpveExGd2lkWEpzWENJNlhDSm9kSFJ3Y3pwY1hGd3ZYRnhjTDNSaGJHc3ViM0JsYm0x
+eWN5NXZjbWRjWEZ3dmRGeGNYQzk1YjNWeUxYQnZjM1F0YVc0dFluVnlaMlZ5YUdGMWN5MXVaWGN0
+Y21WemRHRjFjbUZ1ZEMxc2RXNWphQzEyWlc1MVpWeGNYQzgyTnpSY1hGd3ZNVndpTEZ3aWFXUmNJ
+anBjSW1RMVltTXdOMk5rTkRSalpEUTRNR000WVRnMk16bGpaV0kxTnpnelltWTJYQ0lzWENKMWNt
+eGZhV1J6WENJNlcxd2lZamMyWW1JNU5HVXhZamN5T1dVNU5tVTBaV1ZqT0RSa1pqSTBPREF6TUdJ
+Mk1tRm1NakJqTkZ3aVhYMGlmUSI+DQo8Yj48c3BhbiBzdHlsZT0iY29sb3I6IzAwNjY5OTt0ZXh0
+LWRlY29yYXRpb246bm9uZSI+aHR0cHM6Ly90YWxrLm9wZW5tcnMub3JnL3QveW91ci1wb3N0LWlu
+LWJ1cmdlcmhhdXMtbmV3LXJlc3RhdXJhbnQtbHVuY2gtdmVudWUvNjc0LzE8L3NwYW4+PC9iPjwv
+YT4gaW4geW91ciBicm93c2VyLjxvOnA+PC9vOnA+PC9zcGFuPjwvcD4NCjwvZGl2Pg0KPGRpdj4N
+CjxwPjxzcGFuIHN0eWxlPSJjb2xvcjojNjY2NjY2Ij5UbyB1bnN1YnNjcmliZSBmcm9tIHRoZXNl
+IGVtYWlscywgdmlzaXQgeW91ciA8YSBocmVmPSJodHRwOi8vY2wub3Blbm1ycy5vcmcvdHJhY2sv
+Y2xpY2svMzAwMzk5MDUvdGFsay5vcGVubXJzLm9yZz9wPWV5SnpJam9pZFV4dVdsZzVWRmMwT1da
+V1MwWTRiRmRMZG1seVdHc3hUVjl6SWl3aWRpSTZNU3dpY0NJNkludGNJblZjSWpvek1EQXpPVGt3
+TlN4Y0luWmNJam94TEZ3aWRYSnNYQ0k2WENKb2RIUndjenBjWEZ3dlhGeGNMM1JoYkdzdWIzQmxi
+bTF5Y3k1dmNtZGNYRnd2YlhsY1hGd3ZjSEpsWm1WeVpXNWpaWE5jSWl4Y0ltbGtYQ0k2WENKa05X
+SmpNRGRqWkRRMFkyUTBPREJqT0dFNE5qTTVZMlZpTlRjNE0ySm1ObHdpTEZ3aWRYSnNYMmxrYzF3
+aU9sdGNJbUk0TVdVd1pqQTFORFk1TkRNME56Z3lNMkZtTWpBMk5qRmpaamMzWkdOaU4yTmhZemRt
+TWpKY0lsMTlJbjAiPg0KPGI+PHNwYW4gc3R5bGU9ImNvbG9yOiMwMDY2OTk7dGV4dC1kZWNvcmF0
+aW9uOm5vbmUiPnVzZXIgcHJlZmVyZW5jZXM8L3NwYW4+PC9iPjwvYT4uPG86cD48L286cD48L3Nw
+YW4+PC9wPg0KPC9kaXY+DQo8L2Rpdj4NCjxwIGNsYXNzPSJNc29Ob3JtYWwiPjxpbWcgYm9yZGVy
+PSIwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBpZD0iX3gwMDAwX2kxMDI2IiBzcmM9Imh0dHA6Ly9j
+bC5vcGVubXJzLm9yZy90cmFjay9vcGVuLnBocD91PTMwMDM5OTA1JmFtcDtpZD1kNWJjMDdjZDQ0
+Y2Q0ODBjOGE4NjM5Y2ViNTc4M2JmNiI+PG86cD48L286cD48L3A+DQo8L2Rpdj4NCjwvYm9keT4N
+CjwvaHRtbD4NCg==
+
+--_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_--
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 73f5470cf35..92053e5a7c6 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe ApplicationHelper do
+ include UploadHelpers
+
describe 'current_controller?' do
it 'returns true when controller matches argument' do
stub_controller_name('foo')
@@ -52,12 +54,10 @@ describe ApplicationHelper do
end
describe 'project_icon' do
- let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
-
it 'returns an url for the avatar' do
- project = create(:project, avatar: File.open(avatar_file_path))
+ project = create(:project, avatar: File.open(uploaded_image_temp_path))
- avatar_url = "http://localhost/uploads/project/avatar/#{project.id}/banana_sample.gif"
+ avatar_url = "http://#{Gitlab.config.gitlab.host}/uploads/project/avatar/#{project.id}/banana_sample.gif"
expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).
to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />"
end
@@ -67,17 +67,15 @@ describe ApplicationHelper do
allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true)
- avatar_url = 'http://localhost' + namespace_project_avatar_path(project.namespace, project)
+ avatar_url = "http://#{Gitlab.config.gitlab.host}#{namespace_project_avatar_path(project.namespace, project)}"
expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).to match(
image_tag(avatar_url))
end
end
describe 'avatar_icon' do
- let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
-
it 'returns an url for the avatar' do
- user = create(:user, avatar: File.open(avatar_file_path))
+ user = create(:user, avatar: File.open(uploaded_image_temp_path))
expect(helper.avatar_icon(user.email).to_s).
to match("/uploads/user/avatar/#{user.id}/banana_sample.gif")
@@ -88,7 +86,7 @@ describe ApplicationHelper do
# Must be stubbed after the stub above, and separately
stub_config_setting(url: Settings.send(:build_gitlab_url))
- user = create(:user, avatar: File.open(avatar_file_path))
+ user = create(:user, avatar: File.open(uploaded_image_temp_path))
expect(helper.avatar_icon(user.email).to_s).
to match("/gitlab/uploads/user/avatar/#{user.id}/banana_sample.gif")
@@ -102,7 +100,7 @@ describe ApplicationHelper do
describe 'using a User' do
it 'returns an URL for the avatar' do
- user = create(:user, avatar: File.open(avatar_file_path))
+ user = create(:user, avatar: File.open(uploaded_image_temp_path))
expect(helper.avatar_icon(user).to_s).
to match("/uploads/user/avatar/#{user.id}/banana_sample.gif")
@@ -218,42 +216,24 @@ describe ApplicationHelper do
end
it 'includes a default js-timeago class' do
- expect(element.attr('class')).to eq 'js-timeago js-timeago-pending'
+ expect(element.attr('class')).to eq 'js-timeago'
end
it 'accepts a custom html_class' do
expect(element(html_class: 'custom_class').attr('class')).
- to eq 'js-timeago custom_class js-timeago-pending'
+ to eq 'js-timeago custom_class'
end
it 'accepts a custom tooltip placement' do
expect(element(placement: 'bottom').attr('data-placement')).to eq 'bottom'
end
- it 're-initializes timeago Javascript' do
- el = element.next_element
-
- expect(el.name).to eq 'script'
- expect(el.text).to include "$('.js-timeago-pending').removeClass('js-timeago-pending').timeago()"
- end
-
- it 'allows the script tag to be excluded' do
- expect(element(skip_js: true)).not_to include 'script'
- end
-
it 'converts to Time' do
expect { helper.time_ago_with_tooltip(Date.today) }.not_to raise_error
end
- it 'add class for the short format and includes inline script' do
+ it 'add class for the short format' do
timeago_element = element(short_format: 'short')
- expect(timeago_element.attr('class')).to eq 'js-short-timeago js-timeago-pending'
- script_element = timeago_element.next_element
- expect(script_element.name).to eq 'script'
- end
-
- it 'add class for the short format and does not include inline script' do
- timeago_element = element(short_format: 'short', skip_js: true)
expect(timeago_element.attr('class')).to eq 'js-short-timeago'
expect(timeago_element.next_element).to eq nil
end
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index 9c7c79f57c6..468bcc7badc 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -60,15 +60,58 @@ describe DiffHelper do
end
describe '#diff_line_content' do
- it 'returns non breaking space when line is empty' do
- expect(diff_line_content(nil)).to eq(' &nbsp;')
- end
-
- it 'returns the line itself' do
- expect(diff_line_content(diff_file.diff_lines.first.text)).
- to eq('@@ -6,12 +6,18 @@ module Popen')
- expect(diff_line_content(diff_file.diff_lines.first.type)).to eq('match')
- expect(diff_file.diff_lines.first.new_pos).to eq(6)
+ context 'when the line is empty' do
+ it 'returns a non breaking space' do
+ expect(diff_line_content(nil)).to eq('&nbsp;')
+ end
+
+ it 'returns an HTML-safe string' do
+ expect(diff_line_content(nil)).to be_html_safe
+ end
+ end
+
+ context 'when the line is not empty' do
+ context 'when the line starts with +, -, or a space' do
+ it 'strips the first character' do
+ expect(diff_line_content('+new line')).to eq('new line')
+ expect(diff_line_content('-new line')).to eq('new line')
+ expect(diff_line_content(' new line')).to eq('new line')
+ end
+
+ context 'when the line is HTML-safe' do
+ it 'returns an HTML-safe string' do
+ expect(diff_line_content('+new line'.html_safe)).to be_html_safe
+ expect(diff_line_content('-new line'.html_safe)).to be_html_safe
+ expect(diff_line_content(' new line'.html_safe)).to be_html_safe
+ end
+ end
+
+ context 'when the line is not HTML-safe' do
+ it 'returns a non-HTML-safe string' do
+ expect(diff_line_content('+new line')).not_to be_html_safe
+ expect(diff_line_content('-new line')).not_to be_html_safe
+ expect(diff_line_content(' new line')).not_to be_html_safe
+ end
+ end
+ end
+
+ context 'when the line does not start with a +, -, or a space' do
+ it 'returns the string' do
+ expect(diff_line_content('@@ -6,12 +6,18 @@ module Popen')).to eq('@@ -6,12 +6,18 @@ module Popen')
+ end
+
+ context 'when the line is HTML-safe' do
+ it 'returns an HTML-safe string' do
+ expect(diff_line_content('@@ -6,12 +6,18 @@ module Popen'.html_safe)).to be_html_safe
+ end
+ end
+
+ context 'when the line is not HTML-safe' do
+ it 'returns a non-HTML-safe string' do
+ expect(diff_line_content('@@ -6,12 +6,18 @@ module Popen')).not_to be_html_safe
+ end
+ end
+ end
end
end
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index 5368e5fab06..1d494edcd3b 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -113,7 +113,7 @@ describe GitlabMarkdownHelper do
it 'replaces commit message with emoji to link' do
actual = link_to_gfm(':book:Book', '/foo')
expect(actual).
- to eq %Q(<img class="emoji" title=":book:" alt=":book:" src="http://localhost/assets/1F4D6.png" height="20" width="20" align="absmiddle"><a href="/foo">Book</a>)
+ to eq %Q(<img class="emoji" title=":book:" alt=":book:" src="http://#{Gitlab.config.gitlab.host}/assets/1F4D6.png" height="20" width="20" align="absmiddle"><a href="/foo">Book</a>)
end
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 62cc10f579a..a4f08dc4af0 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -114,4 +114,25 @@ describe IssuablesHelper do
end
end
end
+
+ describe '#issuable_filter_present?' do
+ it 'returns true when any key is present' do
+ allow(helper).to receive(:params).and_return(
+ ActionController::Parameters.new(milestone_title: 'Velit consectetur asperiores natus delectus.',
+ project_id: 'gitlabhq',
+ scope: 'all')
+ )
+
+ expect(helper.issuable_filter_present?).to be_truthy
+ end
+
+ it 'returns false when no key is present' do
+ allow(helper).to receive(:params).and_return(
+ ActionController::Parameters.new(project_id: 'gitlabhq',
+ scope: 'all')
+ )
+
+ expect(helper.issuable_filter_present?).to be_falsey
+ end
+ end
end
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index d30daf47543..7cf535fadae 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -7,7 +7,7 @@ describe LabelsHelper do
context 'without subject' do
it "uses the label's project" do
- expect(link_to_label(label)).to match %r{<a href="/#{label.project.to_reference}/issues\?label_name%5B%5D=#{label.name}">.*</a>}
+ expect(link_to_label(label)).to match %r{<a href="/#{label.project.path_with_namespace}/issues\?label_name%5B%5D=#{label.name}">.*</a>}
end
end
@@ -32,7 +32,7 @@ describe LabelsHelper do
['issue', :issue, 'merge_request', :merge_request].each do |type|
context "set to #{type}" do
it 'links to correct page' do
- expect(link_to_label(label, type: type)).to match %r{<a href="/#{label.project.to_reference}/#{type.to_s.pluralize}\?label_name%5B%5D=#{label.name}">.*</a>}
+ expect(link_to_label(label, type: type)).to match %r{<a href="/#{label.project.path_with_namespace}/#{type.to_s.pluralize}\?label_name%5B%5D=#{label.name}">.*</a>}
end
end
end
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index 6703d88e357..33934cdf8b1 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -10,12 +10,12 @@ describe MembersHelper do
end
describe '#remove_member_message' do
- let(:requester) { build(:user) }
- let(:project) { create(:empty_project, :public) }
+ let(:requester) { create(:user) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:project_member) { build(:project_member, project: project) }
let(:project_member_invite) { build(:project_member, project: project).tap { |m| m.generate_invite_token! } }
let(:project_member_request) { project.request_access(requester) }
- let(:group) { create(:group) }
+ let(:group) { create(:group, :access_requestable) }
let(:group_member) { build(:group_member, group: group) }
let(:group_member_invite) { build(:group_member, group: group).tap { |m| m.generate_invite_token! } }
let(:group_member_request) { group.request_access(requester) }
@@ -31,11 +31,11 @@ describe MembersHelper do
end
describe '#remove_member_title' do
- let(:requester) { build(:user) }
- let(:project) { create(:empty_project, :public) }
+ let(:requester) { create(:user) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:project_member) { build(:project_member, project: project) }
let(:project_member_request) { project.request_access(requester) }
- let(:group) { create(:group) }
+ let(:group) { create(:group, :access_requestable) }
let(:group_member) { build(:group_member, group: group) }
let(:group_member_request) { group.request_access(requester) }
diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb
index 28c2268f8d0..ea744dbb629 100644
--- a/spec/helpers/milestones_helper_spec.rb
+++ b/spec/helpers/milestones_helper_spec.rb
@@ -1,6 +1,25 @@
require 'spec_helper'
describe MilestonesHelper do
+ describe "#milestone_date_range" do
+ def result_for(*args)
+ milestone_date_range(build(:milestone, *args))
+ end
+
+ let(:yesterday) { Date.yesterday }
+ let(:tomorrow) { yesterday + 2 }
+ let(:format) { '%b %-d, %Y' }
+ let(:yesterday_formatted) { yesterday.strftime(format) }
+ let(:tomorrow_formatted) { tomorrow.strftime(format) }
+
+ it { expect(result_for(due_date: nil, start_date: nil)).to be_nil }
+ it { expect(result_for(due_date: tomorrow)).to eq("expires on #{tomorrow_formatted}") }
+ it { expect(result_for(due_date: yesterday)).to eq("expired on #{yesterday_formatted}") }
+ it { expect(result_for(start_date: tomorrow)).to eq("starts on #{tomorrow_formatted}") }
+ it { expect(result_for(start_date: yesterday)).to eq("started on #{yesterday_formatted}") }
+ it { expect(result_for(start_date: yesterday, due_date: tomorrow)).to eq("#{yesterday_formatted} - #{tomorrow_formatted}") }
+ end
+
describe '#milestone_counts' do
let(:project) { FactoryGirl.create(:project) }
let(:counts) { helper.milestone_counts(project.milestones) }
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index 2f9291afc3f..77841e85223 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -85,4 +85,45 @@ describe PreferencesHelper do
and_return(double('user', messages))
end
end
+
+ describe '#default_project_view' do
+ context 'user not signed in' do
+ before do
+ helper.instance_variable_set(:@project, project)
+ stub_user
+ end
+
+ context 'when repository is empty' do
+ let(:project) { create(:project_empty_repo, :public) }
+
+ it 'returns activity if user has repository access' do
+ allow(helper).to receive(:can?).with(nil, :download_code, project).and_return(true)
+
+ expect(helper.default_project_view).to eq('activity')
+ end
+
+ it 'returns activity if user does not have repository access' do
+ allow(helper).to receive(:can?).with(nil, :download_code, project).and_return(false)
+
+ expect(helper.default_project_view).to eq('activity')
+ end
+ end
+
+ context 'when repository is not empty' do
+ let(:project) { create(:project, :public) }
+
+ it 'returns readme if user has repository access' do
+ allow(helper).to receive(:can?).with(nil, :download_code, project).and_return(true)
+
+ expect(helper.default_project_view).to eq('readme')
+ end
+
+ it 'returns activity if user does not have repository access' do
+ allow(helper).to receive(:can?).with(nil, :download_code, project).and_return(false)
+
+ expect(helper.default_project_view).to eq('activity')
+ end
+ end
+ end
+ end
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 64aa41020c9..4b2ca3514f8 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -6,38 +6,6 @@ describe SearchHelper do
str
end
- describe 'parsing result' do
- let(:project) { create(:project) }
- let(:repository) { project.repository }
- let(:results) { repository.search_files('feature', 'master') }
- let(:search_result) { results.first }
-
- subject { helper.parse_search_result(search_result) }
-
- it "returns a valid OpenStruct object" do
- is_expected.to be_an OpenStruct
- expect(subject.filename).to eq('CHANGELOG')
- expect(subject.basename).to eq('CHANGELOG')
- expect(subject.ref).to eq('master')
- expect(subject.startline).to eq(188)
- expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n")
- end
-
- context "when filename has extension" do
- let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" }
-
- it { expect(subject.filename).to eq('CONTRIBUTE.md') }
- it { expect(subject.basename).to eq('CONTRIBUTE') }
- end
-
- context "when file under directory" do
- let(:search_result) { "master:a/b/c.md:5:a b c\n" }
-
- it { expect(subject.filename).to eq('a/b/c.md') }
- it { expect(subject.basename).to eq('a/b/c') }
- end
- end
-
describe 'search_autocomplete_source' do
context "with no current user" do
before do
diff --git a/spec/helpers/sidekiq_helper_spec.rb b/spec/helpers/sidekiq_helper_spec.rb
index d60839b78ec..f86e496740a 100644
--- a/spec/helpers/sidekiq_helper_spec.rb
+++ b/spec/helpers/sidekiq_helper_spec.rb
@@ -30,6 +30,29 @@ describe SidekiqHelper do
expect(parts).to eq(['55137', '10.0', '2.1', 'S+', '2:30pm', 'sidekiq 4.1.4 gitlab [0 of 25 busy]'])
end
+ it 'parses OSX output' do
+ line = ' 1641 1.5 3.8 S+ 4:04PM sidekiq 4.2.1 gitlab [0 of 25 busy]'
+ parts = helper.parse_sidekiq_ps(line)
+
+ expect(parts).to eq(['1641', '1.5', '3.8', 'S+', '4:04PM', 'sidekiq 4.2.1 gitlab [0 of 25 busy]'])
+ end
+
+ it 'parses Ubuntu output' do
+ # Ubuntu Linux 16.04 LTS / procps-3.3.10-4ubuntu2
+ line = ' 938 1.4 2.5 Sl+ 21:23:21 sidekiq 4.2.1 gitlab [0 of 25 busy] '
+ parts = helper.parse_sidekiq_ps(line)
+
+ expect(parts).to eq(['938', '1.4', '2.5', 'Sl+', '21:23:21', 'sidekiq 4.2.1 gitlab [0 of 25 busy]'])
+ end
+
+ it 'parses Debian output' do
+ # Debian Linux Wheezy/Jessie
+ line = '17725 1.0 12.1 Ssl 19:20:15 sidekiq 4.2.1 gitlab-rails [0 of 25 busy] '
+ parts = helper.parse_sidekiq_ps(line)
+
+ expect(parts).to eq(['17725', '1.0', '12.1', 'Ssl', '19:20:15', 'sidekiq 4.2.1 gitlab-rails [0 of 25 busy]'])
+ end
+
it 'does fail gracefully on line not matching the format' do
line = '55137 10.0 2.1 S+ 2:30pm something'
parts = helper.parse_sidekiq_ps(line)
diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc
new file mode 100644
index 00000000000..7792acffac2
--- /dev/null
+++ b/spec/javascripts/.eslintrc
@@ -0,0 +1,15 @@
+{
+ "plugins": ["jasmine"],
+ "env": {
+ "jasmine": true
+ },
+ "extends": "plugin:jasmine/recommended",
+ "rules": {
+ "prefer-arrow-callback": 0,
+ "func-names": 0
+ },
+ "globals": {
+ "fixture": false,
+ "spyOnEvent": false
+ }
+}
diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6
index a3171353bfb..49e56249565 100644
--- a/spec/javascripts/abuse_reports_spec.js.es6
+++ b/spec/javascripts/abuse_reports_spec.js.es6
@@ -1,42 +1,44 @@
-/* eslint-disable */
+/*= require lib/utils/text_utility */
/*= require abuse_reports */
-/*= require jquery */
-
((global) => {
- const FIXTURE = 'abuse_reports.html';
- const MAX_MESSAGE_LENGTH = 500;
+ describe('Abuse Reports', () => {
+ const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw';
+ const MAX_MESSAGE_LENGTH = 500;
+
+ let messages;
- function assertMaxLength($message) {
- expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
- }
+ const assertMaxLength = $message => expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
+ const findMessage = searchText => messages.filter(
+ (index, element) => element.innerText.indexOf(searchText) > -1,
+ ).first();
- describe('Abuse Reports', function() {
fixture.preload(FIXTURE);
- beforeEach(function() {
+ beforeEach(function () {
fixture.load(FIXTURE);
- new global.AbuseReports();
+ this.abuseReports = new global.AbuseReports();
+ messages = $('.abuse-reports .message');
});
- it('should truncate long messages', function() {
- const $longMessage = $('#long');
+
+ it('should truncate long messages', () => {
+ const $longMessage = findMessage('LONG MESSAGE');
expect($longMessage.data('original-message')).toEqual(jasmine.anything());
assertMaxLength($longMessage);
});
- it('should not truncate short messages', function() {
- const $shortMessage = $('#short');
+ it('should not truncate short messages', () => {
+ const $shortMessage = findMessage('SHORT MESSAGE');
expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything());
});
- it('should allow clicking a truncated message to expand and collapse the full message', function() {
- const $longMessage = $('#long');
+ it('should allow clicking a truncated message to expand and collapse the full message', () => {
+ const $longMessage = findMessage('LONG MESSAGE');
$longMessage.click();
expect($longMessage.data('original-message').length).toEqual($longMessage.text().length);
$longMessage.click();
assertMaxLength($longMessage);
});
});
-
})(window.gl);
diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6
index 9d855ef1060..192da4ee8d9 100644
--- a/spec/javascripts/activities_spec.js.es6
+++ b/spec/javascripts/activities_spec.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-unused-expressions, comma-spacing, prefer-const, no-prototype-builtins, semi, no-new, keyword-spacing, no-plusplus, no-shadow, max-len */
+
/*= require js.cookie.js */
/*= require jquery.endless-scroll.js */
/*= require pager */
@@ -35,7 +36,7 @@
describe('Activities', () => {
beforeEach(() => {
fixture.load(fixtureTemplate);
- new Activities();
+ new gl.Activities();
});
for(let i = 0; i < filters.length; i++) {
diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js
deleted file mode 100644
index 16e908f3a81..00000000000
--- a/spec/javascripts/application_spec.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/* eslint-disable */
-
-/*= require lib/utils/common_utils */
-
-(function() {
- describe('Application', function() {
- return describe('disable buttons', function() {
- fixture.preload('application.html');
- beforeEach(function() {
- return fixture.load('application.html');
- });
- it('should prevent default action for disabled buttons', function() {
- var $button, isClicked;
- gl.utils.preventDisabledButtons();
- isClicked = false;
- $button = $('#test-button');
- expect($button).toExist();
- $button.click(function() {
- return isClicked = true;
- });
- $button.trigger('click');
- return expect(isClicked).toBe(false);
- });
-
- it('should be on the same page if a disabled link clicked', function() {
- var locationBeforeLinkClick, $link;
- locationBeforeLinkClick = window.location.href;
- gl.utils.preventDisabledButtons();
- $link = $('#test-link');
- expect($link).toExist();
- $link.click();
- return expect(window.location.href).toBe(locationBeforeLinkClick);
- });
- });
- });
-
-}).call(this);
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index 3d705e1cb2e..89201c8cb8b 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, padded-blocks, max-len */
+/* global AwardsHandler */
/*= require awards_handler */
/*= require jquery */
@@ -33,9 +34,9 @@
};
describe('AwardsHandler', function() {
- fixture.preload('awards_handler.html');
+ fixture.preload('issues/open-issue.html.raw');
beforeEach(function() {
- fixture.load('awards_handler.html');
+ fixture.load('issues/open-issue.html.raw');
awardsHandler = new AwardsHandler;
spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) {
return function(url, emoji, cb) {
@@ -113,7 +114,7 @@
});
describe('::getAwardUrl', function() {
return it('should return the url for request', function() {
- return expect(awardsHandler.getAwardUrl()).toBe('/gitlab-org/gitlab-test/issues/8/toggle_award_emoji');
+ return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji');
});
});
describe('::addAward and ::checkMutuality', function() {
@@ -209,7 +210,7 @@
$('.js-add-award').eq(0).click();
$menu = $('.emoji-menu');
$block = $('.js-awards-block');
- $emoji = $menu.find(".emoji-menu-list-item " + selector);
+ $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + selector);
expect($emoji.length).toBe(1);
expect($block.find(selector).length).toBe(0);
$emoji.click();
@@ -224,7 +225,7 @@
openEmojiMenuAndAddEmoji();
$('.js-add-award').eq(0).click();
$block = $('.js-awards-block');
- $emoji = $('.emoji-menu').find(".emoji-menu-list-item " + selector);
+ $emoji = $('.emoji-menu').find(".emoji-menu-list:not(.frequent-emojis) " + selector);
$emoji.click();
return expect($block.find(selector).length).toBe(0);
});
diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js
index 36254a7370e..b4573e53a4e 100644
--- a/spec/javascripts/behaviors/autosize_spec.js
+++ b/spec/javascripts/behaviors/autosize_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, comma-dangle, no-return-assign, padded-blocks, max-len */
/*= require behaviors/autosize */
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index 7370ccb4a08..0f61000bc37 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */
/*= require behaviors/quick_submit */
diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js
index 32469a4fd1f..c3f4c867d6a 100644
--- a/spec/javascripts/behaviors/requires_input_spec.js
+++ b/spec/javascripts/behaviors/requires_input_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, padded-blocks */
/*= require behaviors/requires_input */
diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6
index 6208c2386b0..b3a1afa28a5 100644
--- a/spec/javascripts/boards/boards_store_spec.js.es6
+++ b/spec/javascripts/boards/boards_store_spec.js.es6
@@ -1,4 +1,11 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, one-var, no-unused-vars, indent */
+/* global Vue */
+/* global BoardService */
+/* global boardsMockInterceptor */
+/* global Cookies */
+/* global listObj */
+/* global listObjDuplicate */
+
//= require jquery
//= require jquery_ujs
//= require js.cookie
@@ -13,8 +20,9 @@
//= require boards/stores/boards_store
//= require ./mock_data
-(() => {
+describe('Store', () => {
beforeEach(() => {
+ Vue.http.interceptors.push(boardsMockInterceptor);
gl.boardService = new BoardService('/test/issue-boards/board', '1');
gl.issueBoards.BoardsStore.create();
@@ -24,145 +32,147 @@
});
});
- describe('Store', () => {
- it('starts with a blank state', () => {
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
- });
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
+ });
- describe('lists', () => {
- it('creates new list without persisting to DB', () => {
- gl.issueBoards.BoardsStore.addList(listObj);
+ it('starts with a blank state', () => {
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
+ });
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
- });
+ describe('lists', () => {
+ it('creates new list without persisting to DB', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
- it('finds list by ID', () => {
- gl.issueBoards.BoardsStore.addList(listObj);
- const list = gl.issueBoards.BoardsStore.findList('id', 1);
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+ });
- expect(list.id).toBe(1);
- });
+ it('finds list by ID', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ const list = gl.issueBoards.BoardsStore.findList('id', 1);
- it('finds list by type', () => {
- gl.issueBoards.BoardsStore.addList(listObj);
- const list = gl.issueBoards.BoardsStore.findList('type', 'label');
+ expect(list.id).toBe(1);
+ });
- expect(list).toBeDefined();
- });
+ it('finds list by type', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ const list = gl.issueBoards.BoardsStore.findList('type', 'label');
- it('finds list limited by type', () => {
- gl.issueBoards.BoardsStore.addList({
- id: 1,
- position: 0,
- title: 'Test',
- list_type: 'backlog'
- });
- const list = gl.issueBoards.BoardsStore.findList('id', 1, 'backlog');
+ expect(list).toBeDefined();
+ });
- expect(list).toBeDefined();
+ it('finds list limited by type', () => {
+ gl.issueBoards.BoardsStore.addList({
+ id: 1,
+ position: 0,
+ title: 'Test',
+ list_type: 'backlog'
});
+ const list = gl.issueBoards.BoardsStore.findList('id', 1, 'backlog');
- it('gets issue when new list added', (done) => {
- gl.issueBoards.BoardsStore.addList(listObj);
- const list = gl.issueBoards.BoardsStore.findList('id', 1);
+ expect(list).toBeDefined();
+ });
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+ it('gets issue when new list added', (done) => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ const list = gl.issueBoards.BoardsStore.findList('id', 1);
- setTimeout(() => {
- expect(list.issues.length).toBe(1);
- expect(list.issues[0].id).toBe(1);
- done();
- }, 0);
- });
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
- it('persists new list', (done) => {
- gl.issueBoards.BoardsStore.new({
- title: 'Test',
- type: 'label',
- label: {
- id: 1,
- title: 'Testing',
- color: 'red',
- description: 'testing;'
- }
- });
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
-
- setTimeout(() => {
- const list = gl.issueBoards.BoardsStore.findList('id', 1);
- expect(list).toBeDefined();
- expect(list.id).toBe(1);
- expect(list.position).toBe(0);
- done();
- }, 0);
- });
+ setTimeout(() => {
+ expect(list.issues.length).toBe(1);
+ expect(list.issues[0].id).toBe(1);
+ done();
+ }, 0);
+ });
- it('check for blank state adding', () => {
- expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true);
+ it('persists new list', (done) => {
+ gl.issueBoards.BoardsStore.new({
+ title: 'Test',
+ type: 'label',
+ label: {
+ id: 1,
+ title: 'Testing',
+ color: 'red',
+ description: 'testing;'
+ }
});
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
- it('check for blank state not adding', () => {
- gl.issueBoards.BoardsStore.addList(listObj);
- expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false);
- });
+ setTimeout(() => {
+ const list = gl.issueBoards.BoardsStore.findList('id', 1);
+ expect(list).toBeDefined();
+ expect(list.id).toBe(1);
+ expect(list.position).toBe(0);
+ done();
+ }, 0);
+ });
- it('check for blank state adding when backlog & done list exist', () => {
- gl.issueBoards.BoardsStore.addList({
- list_type: 'backlog'
- });
- gl.issueBoards.BoardsStore.addList({
- list_type: 'done'
- });
+ it('check for blank state adding', () => {
+ expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true);
+ });
+
+ it('check for blank state not adding', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false);
+ });
- expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true);
+ it('check for blank state adding when backlog & done list exist', () => {
+ gl.issueBoards.BoardsStore.addList({
+ list_type: 'backlog'
+ });
+ gl.issueBoards.BoardsStore.addList({
+ list_type: 'done'
});
- it('adds the blank state', () => {
- gl.issueBoards.BoardsStore.addBlankState();
+ expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true);
+ });
- const list = gl.issueBoards.BoardsStore.findList('type', 'blank', 'blank');
- expect(list).toBeDefined();
- });
+ it('adds the blank state', () => {
+ gl.issueBoards.BoardsStore.addBlankState();
- it('removes list from state', () => {
- gl.issueBoards.BoardsStore.addList(listObj);
+ const list = gl.issueBoards.BoardsStore.findList('type', 'blank', 'blank');
+ expect(list).toBeDefined();
+ });
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+ it('removes list from state', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
- gl.issueBoards.BoardsStore.removeList(1, 'label');
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
- });
+ gl.issueBoards.BoardsStore.removeList(1, 'label');
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
+ });
- it('moves the position of lists', () => {
- const listOne = gl.issueBoards.BoardsStore.addList(listObj),
- listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+ it('moves the position of lists', () => {
+ const listOne = gl.issueBoards.BoardsStore.addList(listObj),
+ listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
- gl.issueBoards.BoardsStore.moveList(listOne, ['2', '1']);
+ gl.issueBoards.BoardsStore.moveList(listOne, ['2', '1']);
- expect(listOne.position).toBe(1);
- });
+ expect(listOne.position).toBe(1);
+ });
- it('moves an issue from one list to another', (done) => {
- const listOne = gl.issueBoards.BoardsStore.addList(listObj),
- listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+ it('moves an issue from one list to another', (done) => {
+ const listOne = gl.issueBoards.BoardsStore.addList(listObj),
+ listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
- setTimeout(() => {
- expect(listOne.issues.length).toBe(1);
- expect(listTwo.issues.length).toBe(1);
+ setTimeout(() => {
+ expect(listOne.issues.length).toBe(1);
+ expect(listTwo.issues.length).toBe(1);
- gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(1));
+ gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(1));
- expect(listOne.issues.length).toBe(0);
- expect(listTwo.issues.length).toBe(1);
+ expect(listOne.issues.length).toBe(0);
+ expect(listTwo.issues.length).toBe(1);
- done();
- }, 0);
- });
+ done();
+ }, 0);
});
});
-})();
+});
diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6
index 90cb8926545..c8a61a0a9b5 100644
--- a/spec/javascripts/boards/issue_spec.js.es6
+++ b/spec/javascripts/boards/issue_spec.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle */
+/* global BoardService */
+/* global ListIssue */
+
//= require jquery
//= require jquery_ujs
//= require js.cookie
diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6
index 1a0427fdd90..7d942ec3d65 100644
--- a/spec/javascripts/boards/list_spec.js.es6
+++ b/spec/javascripts/boards/list_spec.js.es6
@@ -1,4 +1,10 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle */
+/* global Vue */
+/* global boardsMockInterceptor */
+/* global BoardService */
+/* global List */
+/* global listObj */
+
//= require jquery
//= require jquery_ujs
//= require js.cookie
@@ -17,12 +23,17 @@ describe('List model', () => {
let list;
beforeEach(() => {
+ Vue.http.interceptors.push(boardsMockInterceptor);
gl.boardService = new BoardService('/test/issue-boards/board', '1');
gl.issueBoards.BoardsStore.create();
list = new List(listObj);
});
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
+ });
+
it('gets issues when created', (done) => {
setTimeout(() => {
expect(list.issues.length).toBe(1);
diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6
index 80d05e8a1a3..8d3e2237fda 100644
--- a/spec/javascripts/boards/mock_data.js.es6
+++ b/spec/javascripts/boards/mock_data.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, no-unused-vars, quote-props */
+
const listObj = {
id: 1,
position: 0,
@@ -48,10 +49,10 @@ const BoardsMockData = {
}
};
-Vue.http.interceptors.push((request, next) => {
+const boardsMockInterceptor = (request, next) => {
const body = BoardsMockData[request.method][request.url];
next(request.respondWith(JSON.stringify(body), {
status: 200
}));
-});
+};
diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6
new file mode 100644
index 00000000000..133712debab
--- /dev/null
+++ b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6
@@ -0,0 +1,59 @@
+//= require lib/utils/bootstrap_linked_tabs
+
+(() => {
+ describe('Linked Tabs', () => {
+ fixture.preload('linked_tabs');
+
+ beforeEach(() => {
+ fixture.load('linked_tabs');
+ });
+
+ describe('when is initialized', () => {
+ beforeEach(() => {
+ spyOn(window.history, 'replaceState').and.callFake(function () {});
+ });
+
+ it('should activate the tab correspondent to the given action', () => {
+ const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
+ action: 'tab1',
+ defaultAction: 'tab1',
+ parentEl: '.linked-tabs',
+ });
+
+ expect(document.querySelector('#tab1').classList).toContain('active');
+ });
+
+ it('should active the default tab action when the action is show', () => {
+ const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
+ action: 'show',
+ defaultAction: 'tab1',
+ parentEl: '.linked-tabs',
+ });
+
+ expect(document.querySelector('#tab1').classList).toContain('active');
+ });
+ });
+
+ describe('on click', () => {
+ it('should change the url according to the clicked tab', () => {
+ const historySpy = spyOn(history, 'replaceState').and.callFake(() => {});
+
+ const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
+ action: 'show',
+ defaultAction: 'tab1',
+ parentEl: '.linked-tabs',
+ });
+
+ const secondTab = document.querySelector('.linked-tabs li:nth-child(2) a');
+ const newState = secondTab.getAttribute('href') + linkedTabs.currentLocation.search + linkedTabs.currentLocation.hash;
+
+ secondTab.click();
+
+ expect(historySpy).toHaveBeenCalledWith({
+ turbolinks: true,
+ url: newState,
+ }, document.title, newState);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6
new file mode 100644
index 00000000000..3983cad4c13
--- /dev/null
+++ b/spec/javascripts/build_spec.js.es6
@@ -0,0 +1,184 @@
+/* eslint-disable no-new */
+/* global Build */
+/* global Turbolinks */
+
+//= require lib/utils/datetime_utility
+//= require build
+//= require breakpoints
+//= require jquery.nicescroll
+//= require turbolinks
+
+describe('Build', () => {
+ const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`;
+ // see spec/factories/ci/builds.rb
+ const BUILD_TRACE = 'BUILD TRACE';
+ // see lib/ci/ansi2html.rb
+ const INITIAL_BUILD_TRACE_STATE = window.btoa(JSON.stringify({
+ offset: BUILD_TRACE.length, n_open_tags: 0, fg_color: null, bg_color: null, style_mask: 0,
+ }));
+
+ fixture.preload('builds/build-with-artifacts.html.raw');
+
+ beforeEach(() => {
+ fixture.load('builds/build-with-artifacts.html.raw');
+ spyOn($, 'ajax');
+ });
+
+ describe('constructor', () => {
+ beforeEach(() => {
+ jasmine.clock().install();
+ });
+
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ });
+
+ describe('setup', () => {
+ beforeEach(function () {
+ this.build = new Build();
+ });
+
+ it('copies build options', function () {
+ expect(this.build.pageUrl).toBe(BUILD_URL);
+ expect(this.build.buildUrl).toBe(`${BUILD_URL}.json`);
+ expect(this.build.buildStatus).toBe('success');
+ expect(this.build.buildStage).toBe('test');
+ expect(this.build.state).toBe(INITIAL_BUILD_TRACE_STATE);
+ });
+
+ it('only shows the jobs matching the current stage', () => {
+ expect($('.build-job[data-stage="build"]').is(':visible')).toBe(false);
+ expect($('.build-job[data-stage="test"]').is(':visible')).toBe(true);
+ expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
+ });
+
+ it('selects the current stage in the build dropdown menu', () => {
+ expect($('.stage-selection').text()).toBe('test');
+ });
+
+ it('updates the jobs when the build dropdown changes', () => {
+ $('.stage-item:contains("build")').click();
+
+ expect($('.stage-selection').text()).toBe('build');
+ expect($('.build-job[data-stage="build"]').is(':visible')).toBe(true);
+ expect($('.build-job[data-stage="test"]').is(':visible')).toBe(false);
+ expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
+ });
+
+ it('displays the remove date correctly', () => {
+ const removeDateElement = document.querySelector('.js-artifacts-remove');
+ expect(removeDateElement.innerText.trim()).toBe('1 year');
+ });
+ });
+
+ describe('initial build trace', () => {
+ beforeEach(() => {
+ new Build();
+ });
+
+ it('displays the initial build trace', () => {
+ expect($.ajax.calls.count()).toBe(1);
+ const [{ url, dataType, success, context }] = $.ajax.calls.argsFor(0);
+ expect(url).toBe(`${BUILD_URL}.json`);
+ expect(dataType).toBe('json');
+ expect(success).toEqual(jasmine.any(Function));
+
+ success.call(context, { trace_html: '<span>Example</span>', status: 'running' });
+
+ expect($('#build-trace .js-build-output').text()).toMatch(/Example/);
+ });
+
+ it('removes the spinner', () => {
+ const [{ success, context }] = $.ajax.calls.argsFor(0);
+ success.call(context, { trace_html: '<span>Example</span>', status: 'success' });
+
+ expect($('.js-build-refresh').length).toBe(0);
+ });
+ });
+
+ describe('running build', () => {
+ beforeEach(function () {
+ $('.js-build-options').data('buildStatus', 'running');
+ this.build = new Build();
+ spyOn(this.build, 'location').and.returnValue(BUILD_URL);
+ });
+
+ it('updates the build trace on an interval', function () {
+ jasmine.clock().tick(4001);
+
+ expect($.ajax.calls.count()).toBe(2);
+ let [{ url, dataType, success, context }] = $.ajax.calls.argsFor(1);
+ expect(url).toBe(
+ `${BUILD_URL}/trace.json?state=${encodeURIComponent(INITIAL_BUILD_TRACE_STATE)}`,
+ );
+ expect(dataType).toBe('json');
+ expect(success).toEqual(jasmine.any(Function));
+
+ success.call(context, {
+ html: '<span>Update<span>',
+ status: 'running',
+ state: 'newstate',
+ append: true,
+ });
+
+ expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
+ expect(this.build.state).toBe('newstate');
+
+ jasmine.clock().tick(4001);
+
+ expect($.ajax.calls.count()).toBe(3);
+ [{ url, dataType, success, context }] = $.ajax.calls.argsFor(2);
+ expect(url).toBe(`${BUILD_URL}/trace.json?state=newstate`);
+ expect(dataType).toBe('json');
+ expect(success).toEqual(jasmine.any(Function));
+
+ success.call(context, {
+ html: '<span>More</span>',
+ status: 'running',
+ state: 'finalstate',
+ append: true,
+ });
+
+ expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
+ expect(this.build.state).toBe('finalstate');
+ });
+
+ it('replaces the entire build trace', () => {
+ jasmine.clock().tick(4001);
+ let [{ success, context }] = $.ajax.calls.argsFor(1);
+ success.call(context, {
+ html: '<span>Update</span>',
+ status: 'running',
+ append: true,
+ });
+
+ expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
+
+ jasmine.clock().tick(4001);
+ [{ success, context }] = $.ajax.calls.argsFor(2);
+ success.call(context, {
+ html: '<span>Different</span>',
+ status: 'running',
+ append: false,
+ });
+
+ expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
+ expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
+ });
+
+ it('reloads the page when the build is done', () => {
+ spyOn(Turbolinks, 'visit');
+
+ jasmine.clock().tick(4001);
+ const [{ success, context }] = $.ajax.calls.argsFor(1);
+ success.call(context, {
+ html: '<span>Final</span>',
+ status: 'passed',
+ append: true,
+ });
+
+ expect(Turbolinks.visit).toHaveBeenCalledWith(BUILD_URL);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6
index 93f73fa0e9a..aadf6f518a8 100644
--- a/spec/javascripts/dashboard_spec.js.es6
+++ b/spec/javascripts/dashboard_spec.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-new, padded-blocks */
+
/*= require sidebar */
/*= require jquery */
/*= require js.cookie */
diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6
index 9fdbab3a9e9..8ece24555c5 100644
--- a/spec/javascripts/datetime_utility_spec.js.es6
+++ b/spec/javascripts/datetime_utility_spec.js.es6
@@ -1,5 +1,5 @@
-/* eslint-disable */
//= require lib/utils/datetime_utility
+
(() => {
describe('Date time utils', () => {
describe('get day name', () => {
diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6
index 9b2845af608..18805d26ac0 100644
--- a/spec/javascripts/diff_comments_store_spec.js.es6
+++ b/spec/javascripts/diff_comments_store_spec.js.es6
@@ -1,8 +1,11 @@
-/* eslint-disable */
+/* eslint-disable no-extra-semi, 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
+
(() => {
function createDiscussion(noteId = 1, resolved = true) {
CommentsStore.create('a', noteId, true, resolved, 'test');
diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6
new file mode 100644
index 00000000000..4bae3f30bb5
--- /dev/null
+++ b/spec/javascripts/environments/environment_actions_spec.js.es6
@@ -0,0 +1,67 @@
+//= require vue
+//= require environments/components/environment_actions
+
+describe('Actions Component', () => {
+ fixture.preload('environments/element.html');
+
+ beforeEach(() => {
+ fixture.load('environments/element.html');
+ });
+
+ it('should render a dropdown with the provided actions', () => {
+ const actionsMock = [
+ {
+ name: 'bar',
+ play_path: 'https://gitlab.com/play',
+ },
+ {
+ name: 'foo',
+ play_path: '#',
+ },
+ ];
+
+ const component = new window.gl.environmentsList.ActionsComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ actions: actionsMock,
+ playIconSvg: '<svg></svg>',
+ },
+ });
+
+ expect(
+ component.$el.querySelectorAll('.dropdown-menu li').length,
+ ).toEqual(actionsMock.length);
+ expect(
+ component.$el.querySelector('.dropdown-menu li a').getAttribute('href'),
+ ).toEqual(actionsMock[0].play_path);
+ });
+
+ it('should render a dropdown with the provided svg', () => {
+ const actionsMock = [
+ {
+ name: 'bar',
+ play_path: 'https://gitlab.com/play',
+ },
+ {
+ name: 'foo',
+ play_path: '#',
+ },
+ ];
+
+ const component = new window.gl.environmentsList.ActionsComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ actions: actionsMock,
+ playIconSvg: '<svg></svg>',
+ },
+ });
+
+ expect(
+ component.$el.querySelector('.js-dropdown-play-icon-container').children,
+ ).toContain('svg');
+
+ expect(
+ component.$el.querySelector('.js-action-play-icon-container').children,
+ ).toContain('svg');
+ });
+});
diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6
new file mode 100644
index 00000000000..9f82567c35b
--- /dev/null
+++ b/spec/javascripts/environments/environment_external_url_spec.js.es6
@@ -0,0 +1,22 @@
+//= require vue
+//= require environments/components/environment_external_url
+
+describe('External URL Component', () => {
+ fixture.preload('environments/element.html');
+ beforeEach(() => {
+ fixture.load('environments/element.html');
+ });
+
+ it('should link to the provided externalUrl prop', () => {
+ const externalURL = 'https://gitlab.com';
+ const component = new window.gl.environmentsList.ExternalUrlComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ externalUrl: externalURL,
+ },
+ });
+
+ expect(component.$el.getAttribute('href')).toEqual(externalURL);
+ expect(component.$el.querySelector('fa-external-link')).toBeDefined();
+ });
+});
diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6
new file mode 100644
index 00000000000..5d7c6b2411d
--- /dev/null
+++ b/spec/javascripts/environments/environment_item_spec.js.es6
@@ -0,0 +1,229 @@
+//= require vue
+//= require timeago
+//= require environments/components/environment_item
+
+describe('Environment item', () => {
+ fixture.preload('environments/table.html');
+ beforeEach(() => {
+ fixture.load('environments/table.html');
+ });
+
+ describe('When item is folder', () => {
+ let mockItem;
+ let component;
+
+ beforeEach(() => {
+ mockItem = {
+ name: 'review',
+ children: [
+ {
+ name: 'review-app',
+ id: 1,
+ state: 'available',
+ external_url: '',
+ last_deployment: {},
+ created_at: '2016-11-07T11:11:16.525Z',
+ updated_at: '2016-11-10T15:55:58.778Z',
+ },
+ {
+ name: 'production',
+ id: 2,
+ state: 'available',
+ external_url: '',
+ last_deployment: {},
+ created_at: '2016-11-07T11:11:16.525Z',
+ updated_at: '2016-11-10T15:55:58.778Z',
+ },
+ ],
+ };
+
+ component = new window.gl.environmentsList.EnvironmentItem({
+ el: document.querySelector('tr#environment-row'),
+ propsData: {
+ model: mockItem,
+ toggleRow: () => {},
+ canCreateDeployment: false,
+ canReadEnvironment: true,
+ },
+ });
+ });
+
+ it('Should render folder icon and name', () => {
+ expect(component.$el.querySelector('.folder-name').textContent).toContain(mockItem.name);
+ expect(component.$el.querySelector('.folder-icon')).toBeDefined();
+ });
+
+ it('Should render the number of children in a badge', () => {
+ expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.children.length);
+ });
+ });
+
+ describe('when item is not folder', () => {
+ let environment;
+ let component;
+
+ beforeEach(() => {
+ environment = {
+ id: 31,
+ name: 'production',
+ state: 'stopped',
+ external_url: 'http://external.com',
+ environment_type: null,
+ last_deployment: {
+ id: 66,
+ iid: 6,
+ sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ ref: {
+ name: 'master',
+ ref_path: 'root/ci-folders/tree/master',
+ },
+ tag: true,
+ 'last?': true,
+ user: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ commit: {
+ id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ short_id: '500aabcb',
+ title: 'Update .gitlab-ci.yml',
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ created_at: '2016-11-07T18:28:13.000+00:00',
+ message: 'Update .gitlab-ci.yml',
+ author: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ },
+ deployable: {
+ id: 1279,
+ name: 'deploy',
+ build_path: '/root/ci-folders/builds/1279',
+ retry_path: '/root/ci-folders/builds/1279/retry',
+ created_at: '2016-11-29T18:11:58.430Z',
+ updated_at: '2016-11-29T18:11:58.430Z',
+ },
+ manual_actions: [
+ {
+ name: 'action',
+ play_path: '/play',
+ },
+ ],
+ },
+ 'stoppable?': true,
+ environment_path: 'root/ci-folders/environments/31',
+ created_at: '2016-11-07T11:11:16.525Z',
+ updated_at: '2016-11-10T15:55:58.778Z',
+ };
+
+ component = new window.gl.environmentsList.EnvironmentItem({
+ el: document.querySelector('tr#environment-row'),
+ propsData: {
+ model: environment,
+ toggleRow: () => {},
+ canCreateDeployment: true,
+ canReadEnvironment: true,
+ },
+ });
+ });
+
+ it('should render environment name', () => {
+ expect(component.$el.querySelector('.environment-name').textContent).toContain(environment.name);
+ });
+
+ describe('With deployment', () => {
+ it('should render deployment internal id', () => {
+ expect(
+ component.$el.querySelector('.deployment-column span').textContent,
+ ).toContain(environment.last_deployment.iid);
+
+ expect(
+ component.$el.querySelector('.deployment-column span').textContent,
+ ).toContain('#');
+ });
+
+ it('should render last deployment date', () => {
+ const timeagoInstance = new timeago(); // eslint-disable-line
+ const formatedDate = timeagoInstance.format(
+ environment.last_deployment.deployable.created_at,
+ );
+
+ expect(
+ component.$el.querySelector('.environment-created-date-timeago').textContent,
+ ).toContain(formatedDate);
+ });
+
+ describe('With user information', () => {
+ it('should render user avatar with link to profile', () => {
+ expect(
+ component.$el.querySelector('.js-deploy-user-container').getAttribute('href'),
+ ).toEqual(environment.last_deployment.user.web_url);
+ });
+ });
+
+ describe('With build url', () => {
+ it('Should link to build url provided', () => {
+ expect(
+ component.$el.querySelector('.build-link').getAttribute('href'),
+ ).toEqual(environment.last_deployment.deployable.build_path);
+ });
+
+ it('Should render deployable name and id', () => {
+ expect(
+ component.$el.querySelector('.build-link').getAttribute('href'),
+ ).toEqual(environment.last_deployment.deployable.build_path);
+ });
+ });
+
+ describe('With commit information', () => {
+ it('should render commit component', () => {
+ expect(
+ component.$el.querySelector('.js-commit-component'),
+ ).toBeDefined();
+ });
+ });
+ });
+
+ describe('With manual actions', () => {
+ it('Should render actions component', () => {
+ expect(
+ component.$el.querySelector('.js-manual-actions-container'),
+ ).toBeDefined();
+ });
+ });
+
+ describe('With external URL', () => {
+ it('should render external url component', () => {
+ expect(
+ component.$el.querySelector('.js-external-url-container'),
+ ).toBeDefined();
+ });
+ });
+
+ describe('With stop action', () => {
+ it('Should render stop action component', () => {
+ expect(
+ component.$el.querySelector('.js-stop-component-container'),
+ ).toBeDefined();
+ });
+ });
+
+ describe('With retry action', () => {
+ it('Should render rollback component', () => {
+ expect(
+ component.$el.querySelector('.js-rollback-component-container'),
+ ).toBeDefined();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6
new file mode 100644
index 00000000000..77ba0ab38ec
--- /dev/null
+++ b/spec/javascripts/environments/environment_rollback_spec.js.es6
@@ -0,0 +1,48 @@
+//= require vue
+//= require environments/components/environment_rollback
+describe('Rollback Component', () => {
+ fixture.preload('environments/element.html');
+
+ const retryURL = 'https://gitlab.com/retry';
+
+ beforeEach(() => {
+ fixture.load('environments/element.html');
+ });
+
+ it('Should link to the provided retryUrl', () => {
+ const component = new window.gl.environmentsList.RollbackComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ retryUrl: retryURL,
+ isLastDeployment: true,
+ },
+ });
+
+ expect(component.$el.getAttribute('href')).toEqual(retryURL);
+ });
+
+ it('Should render Re-deploy label when isLastDeployment is true', () => {
+ const component = new window.gl.environmentsList.RollbackComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ retryUrl: retryURL,
+ isLastDeployment: true,
+ },
+ });
+
+ expect(component.$el.querySelector('span').textContent).toContain('Re-deploy');
+ });
+
+
+ it('Should render Rollback label when isLastDeployment is false', () => {
+ const component = new window.gl.environmentsList.RollbackComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ retryUrl: retryURL,
+ isLastDeployment: false,
+ },
+ });
+
+ expect(component.$el.querySelector('span').textContent).toContain('Rollback');
+ });
+});
diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6
new file mode 100644
index 00000000000..84a41b2bf46
--- /dev/null
+++ b/spec/javascripts/environments/environment_stop_spec.js.es6
@@ -0,0 +1,28 @@
+//= require vue
+//= require environments/components/environment_stop
+describe('Stop Component', () => {
+ fixture.preload('environments/element.html');
+
+ let stopURL;
+ let component;
+
+ beforeEach(() => {
+ fixture.load('environments/element.html');
+
+ stopURL = '/stop';
+ component = new window.gl.environmentsList.StopComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ stopUrl: stopURL,
+ },
+ });
+ });
+
+ it('should link to the provided URL', () => {
+ expect(component.$el.getAttribute('href')).toEqual(stopURL);
+ });
+
+ it('should have a data-confirm attribute', () => {
+ expect(component.$el.getAttribute('data-confirm')).toEqual('Are you sure you want to stop this environment?');
+ });
+});
diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6
new file mode 100644
index 00000000000..17c00acf63e
--- /dev/null
+++ b/spec/javascripts/environments/environments_store_spec.js.es6
@@ -0,0 +1,71 @@
+/* global environmentsList */
+
+//= require vue
+//= require environments/stores/environments_store
+//= require ./mock_data
+
+(() => {
+ describe('Store', () => {
+ beforeEach(() => {
+ gl.environmentsList.EnvironmentsStore.create();
+ });
+
+ it('should start with a blank state', () => {
+ expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(0);
+ expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(0);
+ expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(0);
+ });
+
+ describe('store environments', () => {
+ beforeEach(() => {
+ gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList);
+ });
+
+ it('should count stopped environments and save the count in the state', () => {
+ expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(1);
+ });
+
+ it('should count available environments and save the count in the state', () => {
+ expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(3);
+ });
+
+ it('should store environments with same environment_type as sibilings', () => {
+ expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(3);
+
+ const parentFolder = gl.environmentsList.EnvironmentsStore.state.environments
+ .filter(env => env.children && env.children.length > 0);
+
+ expect(parentFolder[0].children.length).toBe(2);
+ expect(parentFolder[0].children[0].environment_type).toBe('review');
+ expect(parentFolder[0].children[1].environment_type).toBe('review');
+ expect(parentFolder[0].children[0].name).toBe('test-environment');
+ expect(parentFolder[0].children[1].name).toBe('test-environment-1');
+ });
+
+ it('should sort the environments alphabetically', () => {
+ const { environments } = gl.environmentsList.EnvironmentsStore.state;
+
+ expect(environments[0].name).toBe('production');
+ expect(environments[1].name).toBe('review');
+ expect(environments[1].children[0].name).toBe('test-environment');
+ expect(environments[1].children[1].name).toBe('test-environment-1');
+ expect(environments[2].name).toBe('review_app');
+ });
+ });
+
+ describe('toggleFolder', () => {
+ beforeEach(() => {
+ gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList);
+ });
+
+ it('should toggle the open property for the given environment', () => {
+ gl.environmentsList.EnvironmentsStore.toggleFolder('review');
+
+ const { environments } = gl.environmentsList.EnvironmentsStore.state;
+ const environment = environments.filter(env => env['vue-isChildren'] === true && env.name === 'review');
+
+ expect(environment[0].isOpen).toBe(true);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6
new file mode 100644
index 00000000000..9e16bc3e6a5
--- /dev/null
+++ b/spec/javascripts/environments/mock_data.js.es6
@@ -0,0 +1,135 @@
+/* eslint-disable no-unused-vars */
+const environmentsList = [
+ {
+ id: 31,
+ name: 'production',
+ state: 'available',
+ external_url: 'https://www.gitlab.com',
+ environment_type: null,
+ last_deployment: {
+ id: 64,
+ iid: 5,
+ sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ ref: {
+ name: 'master',
+ ref_url: 'http://localhost:3000/root/ci-folders/tree/master',
+ },
+ tag: false,
+ 'last?': true,
+ user: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ commit: {
+ id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ short_id: '500aabcb',
+ title: 'Update .gitlab-ci.yml',
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ created_at: '2016-11-07T18:28:13.000+00:00',
+ message: 'Update .gitlab-ci.yml',
+ author: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ },
+ deployable: {
+ id: 1278,
+ name: 'build',
+ build_path: '/root/ci-folders/builds/1278',
+ retry_path: '/root/ci-folders/builds/1278/retry',
+ },
+ manual_actions: [],
+ },
+ 'stoppable?': true,
+ environment_path: '/root/ci-folders/environments/31',
+ created_at: '2016-11-07T11:11:16.525Z',
+ updated_at: '2016-11-07T11:11:16.525Z',
+ },
+ {
+ id: 32,
+ name: 'review_app',
+ state: 'stopped',
+ external_url: 'https://www.gitlab.com',
+ environment_type: null,
+ last_deployment: {
+ id: 64,
+ iid: 5,
+ sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ ref: {
+ name: 'master',
+ ref_url: 'http://localhost:3000/root/ci-folders/tree/master',
+ },
+ tag: false,
+ 'last?': true,
+ user: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ commit: {
+ id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ short_id: '500aabcb',
+ title: 'Update .gitlab-ci.yml',
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ created_at: '2016-11-07T18:28:13.000+00:00',
+ message: 'Update .gitlab-ci.yml',
+ author: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ },
+ deployable: {
+ id: 1278,
+ name: 'build',
+ build_path: '/root/ci-folders/builds/1278',
+ retry_path: '/root/ci-folders/builds/1278/retry',
+ },
+ manual_actions: [],
+ },
+ 'stoppable?': false,
+ environment_path: '/root/ci-folders/environments/31',
+ created_at: '2016-11-07T11:11:16.525Z',
+ updated_at: '2016-11-07T11:11:16.525Z',
+ },
+ {
+ id: 33,
+ name: 'test-environment',
+ state: 'available',
+ environment_type: 'review',
+ last_deployment: null,
+ 'stoppable?': true,
+ environment_path: '/root/ci-folders/environments/31',
+ created_at: '2016-11-07T11:11:16.525Z',
+ updated_at: '2016-11-07T11:11:16.525Z',
+ },
+ {
+ id: 34,
+ name: 'test-environment-1',
+ state: 'available',
+ environment_type: 'review',
+ last_deployment: null,
+ 'stoppable?': true,
+ environment_path: '/root/ci-folders/environments/31',
+ created_at: '2016-11-07T11:11:16.525Z',
+ updated_at: '2016-11-07T11:11:16.525Z',
+ },
+];
diff --git a/spec/javascripts/extensions/array_spec.js b/spec/javascripts/extensions/array_spec.js
deleted file mode 100644
index f28983d7764..00000000000
--- a/spec/javascripts/extensions/array_spec.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* eslint-disable */
-
-/*= require extensions/array */
-
-(function() {
- describe('Array extensions', function() {
- describe('first', function() {
- return it('returns the first item', function() {
- var arr;
- arr = [0, 1, 2, 3, 4, 5];
- return expect(arr.first()).toBe(0);
- });
- });
- return describe('last', function() {
- return it('returns the last item', function() {
- var arr;
- arr = [0, 1, 2, 3, 4, 5];
- return expect(arr.last()).toBe(5);
- });
- });
- });
-
-}).call(this);
diff --git a/spec/javascripts/extensions/array_spec.js.es6 b/spec/javascripts/extensions/array_spec.js.es6
new file mode 100644
index 00000000000..2ec759c8e80
--- /dev/null
+++ b/spec/javascripts/extensions/array_spec.js.es6
@@ -0,0 +1,46 @@
+/* eslint-disable space-before-function-paren, no-var, padded-blocks */
+
+/*= require extensions/array */
+
+(function() {
+ describe('Array extensions', function() {
+ describe('first', function() {
+ return it('returns the first item', function() {
+ var arr;
+ arr = [0, 1, 2, 3, 4, 5];
+ return expect(arr.first()).toBe(0);
+ });
+ });
+ describe('last', function() {
+ return it('returns the last item', function() {
+ var arr;
+ arr = [0, 1, 2, 3, 4, 5];
+ return expect(arr.last()).toBe(5);
+ });
+ });
+
+ describe('find', function () {
+ beforeEach(() => {
+ this.arr = [0, 1, 2, 3, 4, 5];
+ });
+
+ it('returns the item that first passes the predicate function', () => {
+ expect(this.arr.find(item => item === 2)).toBe(2);
+ });
+
+ it('returns undefined if no items pass the predicate function', () => {
+ expect(this.arr.find(item => item === 6)).not.toBeDefined();
+ });
+
+ it('error when called on undefined or null', () => {
+ expect(Array.prototype.find.bind(undefined, item => item === 1)).toThrow();
+ expect(Array.prototype.find.bind(null, item => item === 1)).toThrow();
+ });
+
+ it('error when predicate is not a function', () => {
+ expect(Array.prototype.find.bind(this.arr, 1)).toThrow();
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/extensions/element_spec.js.es6 b/spec/javascripts/extensions/element_spec.js.es6
new file mode 100644
index 00000000000..c5b86d35204
--- /dev/null
+++ b/spec/javascripts/extensions/element_spec.js.es6
@@ -0,0 +1,38 @@
+/*= require extensions/element */
+
+(() => {
+ describe('Element extensions', function () {
+ beforeEach(() => {
+ this.element = document.createElement('ul');
+ });
+
+ describe('matches', () => {
+ it('returns true if element matches the selector', () => {
+ expect(this.element.matches('ul')).toBeTruthy();
+ });
+
+ it("returns false if element doesn't match the selector", () => {
+ expect(this.element.matches('.not-an-element')).toBeFalsy();
+ });
+ });
+
+ describe('closest', () => {
+ beforeEach(() => {
+ this.childElement = document.createElement('li');
+ this.element.appendChild(this.childElement);
+ });
+
+ it('returns the closest parent that matches the selector', () => {
+ expect(this.childElement.closest('ul').toString()).toBe(this.element.toString());
+ });
+
+ it('returns itself if it matches the selector', () => {
+ expect(this.childElement.closest('li').toString()).toBe(this.childElement.toString());
+ });
+
+ it('returns undefined if nothing matches the selector', () => {
+ expect(this.childElement.closest('.no-an-element')).toBeFalsy();
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js
index 9c361bb0867..76309930f27 100644
--- a/spec/javascripts/extensions/jquery_spec.js
+++ b/spec/javascripts/extensions/jquery_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, padded-blocks */
/*= require extensions/jquery */
diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6
new file mode 100644
index 00000000000..3b71c255b30
--- /dev/null
+++ b/spec/javascripts/extensions/object_spec.js.es6
@@ -0,0 +1,25 @@
+/*= require extensions/object */
+
+describe('Object extensions', () => {
+ describe('assign', () => {
+ it('merges source object into target object', () => {
+ const targetObj = {};
+ const sourceObj = {
+ foo: 'bar',
+ };
+ Object.assign(targetObj, sourceObj);
+ expect(targetObj.foo).toBe('bar');
+ });
+
+ it('merges object with the same properties', () => {
+ const targetObj = {
+ foo: 'bar',
+ };
+ const sourceObj = {
+ foo: 'baz',
+ };
+ Object.assign(targetObj, sourceObj);
+ expect(targetObj.foo).toBe('baz');
+ });
+ });
+});
diff --git a/spec/javascripts/fixtures/abuse_reports.html.haml b/spec/javascripts/fixtures/abuse_reports.html.haml
deleted file mode 100644
index 2ec302abcb7..00000000000
--- a/spec/javascripts/fixtures/abuse_reports.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-.abuse-reports
- .message#long
- Cat ipsum dolor sit amet, hide head under blanket so no one can see.
- Gate keepers of hell eat and than sleep on your face but hunt by meowing
- loudly at 5am next to human slave food dispenser cats go for world
- domination or chase laser, yet poop on grasses chirp at birds. Cat is love,
- cat is life chase after silly colored fish toys around the house climb a
- tree, wait for a fireman jump to fireman then scratch his face fall asleep
- on the washing machine lies down always hungry so caticus cuteicus. Sit on
- human. Spot something, big eyes, big eyes, crouch, shake butt, prepare to
- pounce sleep in the bathroom sink hiss at vacuum cleaner hide head under
- blanket so no one can see throwup on your pillow.
- .message#short
- Cat ipsum dolor sit amet, groom yourself 4 hours - checked, have your
- beauty sleep 18 hours - checked, be fabulous for the rest of the day -
- checked! for shake treat bag.
diff --git a/spec/javascripts/fixtures/abuse_reports.rb b/spec/javascripts/fixtures/abuse_reports.rb
new file mode 100644
index 00000000000..de673f94d72
--- /dev/null
+++ b/spec/javascripts/fixtures/abuse_reports.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let!(:abuse_report) { create(:abuse_report) }
+ let!(:abuse_report_with_short_message) { create(:abuse_report, message: 'SHORT MESSAGE') }
+ let!(:abuse_report_with_long_message) { create(:abuse_report, message: "LONG MESSAGE\n" * 50) }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('abuse_reports/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'abuse_reports/abuse_reports_list.html.raw' do |example|
+ get :index
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/fixtures/application.html.haml b/spec/javascripts/fixtures/application.html.haml
deleted file mode 100644
index 3fc6114407d..00000000000
--- a/spec/javascripts/fixtures/application.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-%a#test-link.btn.disabled{:href => "/foo"} Test link
-%button#test-button.btn.disabled Test Button
diff --git a/spec/javascripts/fixtures/awards_handler.html.haml b/spec/javascripts/fixtures/awards_handler.html.haml
deleted file mode 100644
index 1ef2e8f8624..00000000000
--- a/spec/javascripts/fixtures/awards_handler.html.haml
+++ /dev/null
@@ -1,52 +0,0 @@
-.issue-details.issuable-details
- .detail-page-description.content-block
- %h2.title Quibusdam sint officiis earum molestiae ipsa autem voluptatem nisi rem.
- .description.js-task-list-container.is-task-list-enabled
- .wiki
- %p Qui exercitationem magnam optio quae fuga earum odio.
- %textarea.hidden.js-task-list-field Qui exercitationem magnam optio quae fuga earum odio.
- %small.edited-text
- .content-block.content-block-small
- .awards.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/issues/8/toggle_award_emoji"}
- %button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"}
- .icon.emoji-icon.emoji-1F44D{"data-aliases" => "", "data-emoji" => "thumbsup", "data-unicode-name" => "1F44D", :title => "thumbsup"}
- %span.award-control-text.js-counter 0
- %button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"}
- .icon.emoji-icon.emoji-1F44E{"data-aliases" => "", "data-emoji" => "thumbsdown", "data-unicode-name" => "1F44E", :title => "thumbsdown"}
- %span.award-control-text.js-counter 0
- .award-menu-holder.js-award-holder
- %button.btn.award-control.js-add-award{:type => "button"}
- %i.fa.fa-smile-o.award-control-icon.award-control-icon-normal
- %i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading
- %span.award-control-text Add
- %section.issuable-discussion
- #notes
- %ul#notes-list.notes.main-notes-list.timeline
- %li#note_348.note.note-row-348.timeline-entry{"data-author-id" => "18", "data-editable" => ""}
- .timeline-entry-inner
- .timeline-icon
- %a{:href => "/u/agustin"}
- %img.avatar.s40{:alt => "", :src => "#"}/
- .timeline-content
- .note-header
- %a.author_link{:href => "/u/agustin"}
- %span.author Brenna Stokes
- .inline.note-headline-light
- @agustin commented
- %a{:href => "#note_348"}
- %time 11 days ago
- .note-actions
- %span.note-role Reporter
- %a.note-action-button.note-emoji-button.js-add-award.js-note-emoji{"data-position" => "right", :href => "#", :title => "Award Emoji"}
- %i.fa.fa-spinner.fa-spin
- %i.fa.fa-smile-o.link-highlight
- .js-task-list-container.note-body.is-task-list-enabled
- .note-text
- %p Suscipit sunt quia quisquam sed eveniet ipsam.
- .note-awards
- .awards.hidden.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/notes/348/toggle_award_emoji"}
- .award-menu-holder.js-award-holder
- %button.btn.award-control.js-add-award{:type => "button"}
- %i.fa.fa-smile-o.award-control-icon.award-control-icon-normal
- %i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading
- %span.award-control-text Add
diff --git a/spec/javascripts/fixtures/builds.rb b/spec/javascripts/fixtures/builds.rb
new file mode 100644
index 00000000000..978e25a1c32
--- /dev/null
+++ b/spec/javascripts/fixtures/builds.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Projects::BuildsController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project_empty_repo, namespace: namespace, path: 'builds-project') }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+ let!(:build_with_artifacts) { create(:ci_build, :success, :artifacts, :trace, pipeline: pipeline, stage: 'test', artifacts_expire_at: Time.now + 18.months) }
+ let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline, stage: 'build') }
+ let!(:pending_build) { create(:ci_build, :pending, pipeline: pipeline, stage: 'deploy') }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('builds/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'builds/build-with-artifacts.html.raw' do |example|
+ get :show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: build_with_artifacts.to_param
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/fixtures/comments.html.haml b/spec/javascripts/fixtures/comments.html.haml
deleted file mode 100644
index cc1f8f15c21..00000000000
--- a/spec/javascripts/fixtures/comments.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-.flash-container.timeline-content
-.timeline-icon.hidden-xs.hidden-sm
- %a.author_link
- %img
-.timeline-content.timeline-content-form
- %form.new-note.js-quick-submit.common-note-form.gfm-form.js-main-target-form
- .md-area
- .md-header
- .md-write-holder
- .zen-backdrop.div-dropzone-wrapper
- .div-dropzone-wrapper
- .div-dropzone.dz-clickable
- %textarea.note-textarea.js-note-text.js-gfm-input.js-autosize.markdown-area
- .note-form-actions.clearfix
- %input.btn.btn-nr.btn-create.append-right-10.comment-btn.js-comment-button{ type: 'submit' }
- %a.btn.btn-nr.btn-reopen.btn-comment.js-note-target-reopen
- Reopen issue
- %a.btn.btn-nr.btn-close.btn-comment.js-note-target-close
- Close issue
- %a.btn.btn-cancel.js-note-discard
- Discard draft \ No newline at end of file
diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js
index 41cf40c29cf..3d776bb9277 100644
--- a/spec/javascripts/fixtures/emoji_menu.js
+++ b/spec/javascripts/fixtures/emoji_menu.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, padded-blocks */
(function() {
window.emojiMenu = "<div class='emoji-menu'>\n <input type=\"text\" name=\"emoji_search\" id=\"emoji_search\" value=\"\" class=\"emoji-search search-input form-control\" />\n <div class='emoji-menu-content'>\n <h5 class='emoji-menu-title'>\n Emoticons\n </h5>\n <ul class='clearfix emoji-menu-list'>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47D\" title=\"alien\" data-aliases=\"\" data-emoji=\"alien\" data-unicode-name=\"1F47D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47C\" title=\"angel\" data-aliases=\"\" data-emoji=\"angel\" data-unicode-name=\"1F47C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A2\" title=\"anger\" data-aliases=\"\" data-emoji=\"anger\" data-unicode-name=\"1F4A2\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F620\" title=\"angry\" data-aliases=\"\" data-emoji=\"angry\" data-unicode-name=\"1F620\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F627\" title=\"anguished\" data-aliases=\"\" data-emoji=\"anguished\" data-unicode-name=\"1F627\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F632\" title=\"astonished\" data-aliases=\"\" data-emoji=\"astonished\" data-unicode-name=\"1F632\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45F\" title=\"athletic_shoe\" data-aliases=\"\" data-emoji=\"athletic_shoe\" data-unicode-name=\"1F45F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F476\" title=\"baby\" data-aliases=\"\" data-emoji=\"baby\" data-unicode-name=\"1F476\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F459\" title=\"bikini\" data-aliases=\"\" data-emoji=\"bikini\" data-unicode-name=\"1F459\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F499\" title=\"blue_heart\" data-aliases=\"\" data-emoji=\"blue_heart\" data-unicode-name=\"1F499\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60A\" title=\"blush\" data-aliases=\"\" data-emoji=\"blush\" data-unicode-name=\"1F60A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A5\" title=\"boom\" data-aliases=\"\" data-emoji=\"boom\" data-unicode-name=\"1F4A5\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F462\" title=\"boot\" data-aliases=\"\" data-emoji=\"boot\" data-unicode-name=\"1F462\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F647\" title=\"bow\" data-aliases=\"\" data-emoji=\"bow\" data-unicode-name=\"1F647\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F466\" title=\"boy\" data-aliases=\"\" data-emoji=\"boy\" data-unicode-name=\"1F466\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F470\" title=\"bride_with_veil\" data-aliases=\"\" data-emoji=\"bride_with_veil\" data-unicode-name=\"1F470\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4BC\" title=\"briefcase\" data-aliases=\"\" data-emoji=\"briefcase\" data-unicode-name=\"1F4BC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F494\" title=\"broken_heart\" data-aliases=\"\" data-emoji=\"broken_heart\" data-unicode-name=\"1F494\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F464\" title=\"bust_in_silhouette\" data-aliases=\"\" data-emoji=\"bust_in_silhouette\" data-unicode-name=\"1F464\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F465\" title=\"busts_in_silhouette\" data-aliases=\"\" data-emoji=\"busts_in_silhouette\" data-unicode-name=\"1F465\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44F\" title=\"clap\" data-aliases=\"\" data-emoji=\"clap\" data-unicode-name=\"1F44F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F302\" title=\"closed_umbrella\" data-aliases=\"\" data-emoji=\"closed_umbrella\" data-unicode-name=\"1F302\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F630\" title=\"cold_sweat\" data-aliases=\"\" data-emoji=\"cold_sweat\" data-unicode-name=\"1F630\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F616\" title=\"confounded\" data-aliases=\"\" data-emoji=\"confounded\" data-unicode-name=\"1F616\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F615\" title=\"confused\" data-aliases=\"\" data-emoji=\"confused\" data-unicode-name=\"1F615\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F477\" title=\"construction_worker\" data-aliases=\"\" data-emoji=\"construction_worker\" data-unicode-name=\"1F477\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46E\" title=\"cop\" data-aliases=\"\" data-emoji=\"cop\" data-unicode-name=\"1F46E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46B\" title=\"couple\" data-aliases=\"\" data-emoji=\"couple\" data-unicode-name=\"1F46B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F491\" title=\"couple_with_heart\" data-aliases=\"\" data-emoji=\"couple_with_heart\" data-unicode-name=\"1F491\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48F\" title=\"couplekiss\" data-aliases=\"\" data-emoji=\"couplekiss\" data-unicode-name=\"1F48F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F451\" title=\"crown\" data-aliases=\"\" data-emoji=\"crown\" data-unicode-name=\"1F451\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F622\" title=\"cry\" data-aliases=\"\" data-emoji=\"cry\" data-unicode-name=\"1F622\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63F\" title=\"crying_cat_face\" data-aliases=\"\" data-emoji=\"crying_cat_face\" data-unicode-name=\"1F63F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F498\" title=\"cupid\" data-aliases=\"\" data-emoji=\"cupid\" data-unicode-name=\"1F498\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F483\" title=\"dancer\" data-aliases=\"\" data-emoji=\"dancer\" data-unicode-name=\"1F483\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46F\" title=\"dancers\" data-aliases=\"\" data-emoji=\"dancers\" data-unicode-name=\"1F46F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A8\" title=\"dash\" data-aliases=\"\" data-emoji=\"dash\" data-unicode-name=\"1F4A8\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61E\" title=\"disappointed\" data-aliases=\"\" data-emoji=\"disappointed\" data-unicode-name=\"1F61E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F625\" title=\"disappointed_relieved\" data-aliases=\"\" data-emoji=\"disappointed_relieved\" data-unicode-name=\"1F625\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AB\" title=\"dizzy\" data-aliases=\"\" data-emoji=\"dizzy\" data-unicode-name=\"1F4AB\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F635\" title=\"dizzy_face\" data-aliases=\"\" data-emoji=\"dizzy_face\" data-unicode-name=\"1F635\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F457\" title=\"dress\" data-aliases=\"\" data-emoji=\"dress\" data-unicode-name=\"1F457\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A7\" title=\"droplet\" data-aliases=\"\" data-emoji=\"droplet\" data-unicode-name=\"1F4A7\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F442\" title=\"ear\" data-aliases=\"\" data-emoji=\"ear\" data-unicode-name=\"1F442\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F611\" title=\"expressionless\" data-aliases=\"\" data-emoji=\"expressionless\" data-unicode-name=\"1F611\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F453\" title=\"eyeglasses\" data-aliases=\"\" data-emoji=\"eyeglasses\" data-unicode-name=\"1F453\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F440\" title=\"eyes\" data-aliases=\"\" data-emoji=\"eyes\" data-unicode-name=\"1F440\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46A\" title=\"family\" data-aliases=\"\" data-emoji=\"family\" data-unicode-name=\"1F46A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F628\" title=\"fearful\" data-aliases=\"\" data-emoji=\"fearful\" data-unicode-name=\"1F628\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F525\" title=\"fire\" data-aliases=\":flame:\" data-emoji=\"fire\" data-unicode-name=\"1F525\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270A\" title=\"fist\" data-aliases=\"\" data-emoji=\"fist\" data-unicode-name=\"270A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F633\" title=\"flushed\" data-aliases=\"\" data-emoji=\"flushed\" data-unicode-name=\"1F633\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F463\" title=\"footprints\" data-aliases=\"\" data-emoji=\"footprints\" data-unicode-name=\"1F463\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F626\" title=\"frowning\" data-aliases=\":anguished:\" data-emoji=\"frowning\" data-unicode-name=\"1F626\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48E\" title=\"gem\" data-aliases=\"\" data-emoji=\"gem\" data-unicode-name=\"1F48E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F467\" title=\"girl\" data-aliases=\"\" data-emoji=\"girl\" data-unicode-name=\"1F467\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49A\" title=\"green_heart\" data-aliases=\"\" data-emoji=\"green_heart\" data-unicode-name=\"1F49A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62C\" title=\"grimacing\" data-aliases=\"\" data-emoji=\"grimacing\" data-unicode-name=\"1F62C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F601\" title=\"grin\" data-aliases=\"\" data-emoji=\"grin\" data-unicode-name=\"1F601\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F600\" title=\"grinning\" data-aliases=\"\" data-emoji=\"grinning\" data-unicode-name=\"1F600\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F482\" title=\"guardsman\" data-aliases=\"\" data-emoji=\"guardsman\" data-unicode-name=\"1F482\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F487\" title=\"haircut\" data-aliases=\"\" data-emoji=\"haircut\" data-unicode-name=\"1F487\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45C\" title=\"handbag\" data-aliases=\"\" data-emoji=\"handbag\" data-unicode-name=\"1F45C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F649\" title=\"hear_no_evil\" data-aliases=\"\" data-emoji=\"hear_no_evil\" data-unicode-name=\"1F649\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2764\" title=\"heart\" data-aliases=\"\" data-emoji=\"heart\" data-unicode-name=\"2764\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60D\" title=\"heart_eyes\" data-aliases=\"\" data-emoji=\"heart_eyes\" data-unicode-name=\"1F60D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63B\" title=\"heart_eyes_cat\" data-aliases=\"\" data-emoji=\"heart_eyes_cat\" data-unicode-name=\"1F63B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F493\" title=\"heartbeat\" data-aliases=\"\" data-emoji=\"heartbeat\" data-unicode-name=\"1F493\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F497\" title=\"heartpulse\" data-aliases=\"\" data-emoji=\"heartpulse\" data-unicode-name=\"1F497\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F460\" title=\"high_heel\" data-aliases=\"\" data-emoji=\"high_heel\" data-unicode-name=\"1F460\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62F\" title=\"hushed\" data-aliases=\"\" data-emoji=\"hushed\" data-unicode-name=\"1F62F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47F\" title=\"imp\" data-aliases=\"\" data-emoji=\"imp\" data-unicode-name=\"1F47F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F481\" title=\"information_desk_person\" data-aliases=\"\" data-emoji=\"information_desk_person\" data-unicode-name=\"1F481\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F607\" title=\"innocent\" data-aliases=\"\" data-emoji=\"innocent\" data-unicode-name=\"1F607\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47A\" title=\"japanese_goblin\" data-aliases=\"\" data-emoji=\"japanese_goblin\" data-unicode-name=\"1F47A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F479\" title=\"japanese_ogre\" data-aliases=\"\" data-emoji=\"japanese_ogre\" data-unicode-name=\"1F479\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F456\" title=\"jeans\" data-aliases=\"\" data-emoji=\"jeans\" data-unicode-name=\"1F456\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F602\" title=\"joy\" data-aliases=\"\" data-emoji=\"joy\" data-unicode-name=\"1F602\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F639\" title=\"joy_cat\" data-aliases=\"\" data-emoji=\"joy_cat\" data-unicode-name=\"1F639\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F458\" title=\"kimono\" data-aliases=\"\" data-emoji=\"kimono\" data-unicode-name=\"1F458\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48B\" title=\"kiss\" data-aliases=\"\" data-emoji=\"kiss\" data-unicode-name=\"1F48B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F617\" title=\"kissing\" data-aliases=\"\" data-emoji=\"kissing\" data-unicode-name=\"1F617\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63D\" title=\"kissing_cat\" data-aliases=\"\" data-emoji=\"kissing_cat\" data-unicode-name=\"1F63D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61A\" title=\"kissing_closed_eyes\" data-aliases=\"\" data-emoji=\"kissing_closed_eyes\" data-unicode-name=\"1F61A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F618\" title=\"kissing_heart\" data-aliases=\"\" data-emoji=\"kissing_heart\" data-unicode-name=\"1F618\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F619\" title=\"kissing_smiling_eyes\" data-aliases=\"\" data-emoji=\"kissing_smiling_eyes\" data-unicode-name=\"1F619\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F606\" title=\"laughing\" data-aliases=\":satisfied:\" data-emoji=\"laughing\" data-unicode-name=\"1F606\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F444\" title=\"lips\" data-aliases=\"\" data-emoji=\"lips\" data-unicode-name=\"1F444\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F484\" title=\"lipstick\" data-aliases=\"\" data-emoji=\"lipstick\" data-unicode-name=\"1F484\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48C\" title=\"love_letter\" data-aliases=\"\" data-emoji=\"love_letter\" data-unicode-name=\"1F48C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F468\" title=\"man\" data-aliases=\"\" data-emoji=\"man\" data-unicode-name=\"1F468\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F472\" title=\"man_with_gua_pi_mao\" data-aliases=\"\" data-emoji=\"man_with_gua_pi_mao\" data-unicode-name=\"1F472\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F473\" title=\"man_with_turban\" data-aliases=\"\" data-emoji=\"man_with_turban\" data-unicode-name=\"1F473\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45E\" title=\"mans_shoe\" data-aliases=\"\" data-emoji=\"mans_shoe\" data-unicode-name=\"1F45E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F637\" title=\"mask\" data-aliases=\"\" data-emoji=\"mask\" data-unicode-name=\"1F637\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F486\" title=\"massage\" data-aliases=\"\" data-emoji=\"massage\" data-unicode-name=\"1F486\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AA\" title=\"muscle\" data-aliases=\"\" data-emoji=\"muscle\" data-unicode-name=\"1F4AA\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F485\" title=\"nail_care\" data-aliases=\"\" data-emoji=\"nail_care\" data-unicode-name=\"1F485\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F454\" title=\"necktie\" data-aliases=\"\" data-emoji=\"necktie\" data-unicode-name=\"1F454\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F610\" title=\"neutral_face\" data-aliases=\"\" data-emoji=\"neutral_face\" data-unicode-name=\"1F610\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F645\" title=\"no_good\" data-aliases=\"\" data-emoji=\"no_good\" data-unicode-name=\"1F645\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F636\" title=\"no_mouth\" data-aliases=\"\" data-emoji=\"no_mouth\" data-unicode-name=\"1F636\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F443\" title=\"nose\" data-aliases=\"\" data-emoji=\"nose\" data-unicode-name=\"1F443\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44C\" title=\"ok_hand\" data-aliases=\"\" data-emoji=\"ok_hand\" data-unicode-name=\"1F44C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F646\" title=\"ok_woman\" data-aliases=\"\" data-emoji=\"ok_woman\" data-unicode-name=\"1F646\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F474\" title=\"older_man\" data-aliases=\"\" data-emoji=\"older_man\" data-unicode-name=\"1F474\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F475\" title=\"older_woman\" data-aliases=\":grandma:\" data-emoji=\"older_woman\" data-unicode-name=\"1F475\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F450\" title=\"open_hands\" data-aliases=\"\" data-emoji=\"open_hands\" data-unicode-name=\"1F450\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62E\" title=\"open_mouth\" data-aliases=\"\" data-emoji=\"open_mouth\" data-unicode-name=\"1F62E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F614\" title=\"pensive\" data-aliases=\"\" data-emoji=\"pensive\" data-unicode-name=\"1F614\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F623\" title=\"persevere\" data-aliases=\"\" data-emoji=\"persevere\" data-unicode-name=\"1F623\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64D\" title=\"person_frowning\" data-aliases=\"\" data-emoji=\"person_frowning\" data-unicode-name=\"1F64D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F471\" title=\"person_with_blond_hair\" data-aliases=\"\" data-emoji=\"person_with_blond_hair\" data-unicode-name=\"1F471\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64E\" title=\"person_with_pouting_face\" data-aliases=\"\" data-emoji=\"person_with_pouting_face\" data-unicode-name=\"1F64E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F447\" title=\"point_down\" data-aliases=\"\" data-emoji=\"point_down\" data-unicode-name=\"1F447\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F448\" title=\"point_left\" data-aliases=\"\" data-emoji=\"point_left\" data-unicode-name=\"1F448\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F449\" title=\"point_right\" data-aliases=\"\" data-emoji=\"point_right\" data-unicode-name=\"1F449\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-261D\" title=\"point_up\" data-aliases=\"\" data-emoji=\"point_up\" data-unicode-name=\"261D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F446\" title=\"point_up_2\" data-aliases=\"\" data-emoji=\"point_up_2\" data-unicode-name=\"1F446\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A9\" title=\"poop\" data-aliases=\":shit: :hankey: :poo:\" data-emoji=\"poop\" data-unicode-name=\"1F4A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45D\" title=\"pouch\" data-aliases=\"\" data-emoji=\"pouch\" data-unicode-name=\"1F45D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63E\" title=\"pouting_cat\" data-aliases=\"\" data-emoji=\"pouting_cat\" data-unicode-name=\"1F63E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64F\" title=\"pray\" data-aliases=\"\" data-emoji=\"pray\" data-unicode-name=\"1F64F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F478\" title=\"princess\" data-aliases=\"\" data-emoji=\"princess\" data-unicode-name=\"1F478\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44A\" title=\"punch\" data-aliases=\"\" data-emoji=\"punch\" data-unicode-name=\"1F44A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49C\" title=\"purple_heart\" data-aliases=\"\" data-emoji=\"purple_heart\" data-unicode-name=\"1F49C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45B\" title=\"purse\" data-aliases=\"\" data-emoji=\"purse\" data-unicode-name=\"1F45B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F621\" title=\"rage\" data-aliases=\"\" data-emoji=\"rage\" data-unicode-name=\"1F621\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270B\" title=\"raised_hand\" data-aliases=\"\" data-emoji=\"raised_hand\" data-unicode-name=\"270B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64C\" title=\"raised_hands\" data-aliases=\"\" data-emoji=\"raised_hands\" data-unicode-name=\"1F64C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64B\" title=\"raising_hand\" data-aliases=\"\" data-emoji=\"raising_hand\" data-unicode-name=\"1F64B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-263A\" title=\"relaxed\" data-aliases=\"\" data-emoji=\"relaxed\" data-unicode-name=\"263A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60C\" title=\"relieved\" data-aliases=\"\" data-emoji=\"relieved\" data-unicode-name=\"1F60C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49E\" title=\"revolving_hearts\" data-aliases=\"\" data-emoji=\"revolving_hearts\" data-unicode-name=\"1F49E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F380\" title=\"ribbon\" data-aliases=\"\" data-emoji=\"ribbon\" data-unicode-name=\"1F380\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48D\" title=\"ring\" data-aliases=\"\" data-emoji=\"ring\" data-unicode-name=\"1F48D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3C3\" title=\"runner\" data-aliases=\"\" data-emoji=\"runner\" data-unicode-name=\"1F3C3\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3BD\" title=\"running_shirt_with_sash\" data-aliases=\"\" data-emoji=\"running_shirt_with_sash\" data-unicode-name=\"1F3BD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F461\" title=\"sandal\" data-aliases=\"\" data-emoji=\"sandal\" data-unicode-name=\"1F461\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F631\" title=\"scream\" data-aliases=\"\" data-emoji=\"scream\" data-unicode-name=\"1F631\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F640\" title=\"scream_cat\" data-aliases=\"\" data-emoji=\"scream_cat\" data-unicode-name=\"1F640\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F648\" title=\"see_no_evil\" data-aliases=\"\" data-emoji=\"see_no_evil\" data-unicode-name=\"1F648\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F455\" title=\"shirt\" data-aliases=\"\" data-emoji=\"shirt\" data-unicode-name=\"1F455\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F480\" title=\"skull\" data-aliases=\":skeleton:\" data-emoji=\"skull\" data-unicode-name=\"1F480\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F634\" title=\"sleeping\" data-aliases=\"\" data-emoji=\"sleeping\" data-unicode-name=\"1F634\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62A\" title=\"sleepy\" data-aliases=\"\" data-emoji=\"sleepy\" data-unicode-name=\"1F62A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F604\" title=\"smile\" data-aliases=\"\" data-emoji=\"smile\" data-unicode-name=\"1F604\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F638\" title=\"smile_cat\" data-aliases=\"\" data-emoji=\"smile_cat\" data-unicode-name=\"1F638\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F603\" title=\"smiley\" data-aliases=\"\" data-emoji=\"smiley\" data-unicode-name=\"1F603\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63A\" title=\"smiley_cat\" data-aliases=\"\" data-emoji=\"smiley_cat\" data-unicode-name=\"1F63A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F608\" title=\"smiling_imp\" data-aliases=\"\" data-emoji=\"smiling_imp\" data-unicode-name=\"1F608\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60F\" title=\"smirk\" data-aliases=\"\" data-emoji=\"smirk\" data-unicode-name=\"1F60F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63C\" title=\"smirk_cat\" data-aliases=\"\" data-emoji=\"smirk_cat\" data-unicode-name=\"1F63C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62D\" title=\"sob\" data-aliases=\"\" data-emoji=\"sob\" data-unicode-name=\"1F62D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2728\" title=\"sparkles\" data-aliases=\"\" data-emoji=\"sparkles\" data-unicode-name=\"2728\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F496\" title=\"sparkling_heart\" data-aliases=\"\" data-emoji=\"sparkling_heart\" data-unicode-name=\"1F496\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64A\" title=\"speak_no_evil\" data-aliases=\"\" data-emoji=\"speak_no_evil\" data-unicode-name=\"1F64A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AC\" title=\"speech_balloon\" data-aliases=\"\" data-emoji=\"speech_balloon\" data-unicode-name=\"1F4AC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F31F\" title=\"star2\" data-aliases=\"\" data-emoji=\"star2\" data-unicode-name=\"1F31F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61B\" title=\"stuck_out_tongue\" data-aliases=\"\" data-emoji=\"stuck_out_tongue\" data-unicode-name=\"1F61B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61D\" title=\"stuck_out_tongue_closed_eyes\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_closed_eyes\" data-unicode-name=\"1F61D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61C\" title=\"stuck_out_tongue_winking_eye\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_winking_eye\" data-unicode-name=\"1F61C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60E\" title=\"sunglasses\" data-aliases=\"\" data-emoji=\"sunglasses\" data-unicode-name=\"1F60E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F613\" title=\"sweat\" data-aliases=\"\" data-emoji=\"sweat\" data-unicode-name=\"1F613\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A6\" title=\"sweat_drops\" data-aliases=\"\" data-emoji=\"sweat_drops\" data-unicode-name=\"1F4A6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F605\" title=\"sweat_smile\" data-aliases=\"\" data-emoji=\"sweat_smile\" data-unicode-name=\"1F605\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AD\" title=\"thought_balloon\" data-aliases=\"\" data-emoji=\"thought_balloon\" data-unicode-name=\"1F4AD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44E\" title=\"thumbsdown\" data-aliases=\":-1:\" data-emoji=\"thumbsdown\" data-unicode-name=\"1F44E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44D\" title=\"thumbsup\" data-aliases=\":+1:\" data-emoji=\"thumbsup\" data-unicode-name=\"1F44D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62B\" title=\"tired_face\" data-aliases=\"\" data-emoji=\"tired_face\" data-unicode-name=\"1F62B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F445\" title=\"tongue\" data-aliases=\"\" data-emoji=\"tongue\" data-unicode-name=\"1F445\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3A9\" title=\"tophat\" data-aliases=\"\" data-emoji=\"tophat\" data-unicode-name=\"1F3A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F624\" title=\"triumph\" data-aliases=\"\" data-emoji=\"triumph\" data-unicode-name=\"1F624\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F495\" title=\"two_hearts\" data-aliases=\"\" data-emoji=\"two_hearts\" data-unicode-name=\"1F495\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46C\" title=\"two_men_holding_hands\" data-aliases=\"\" data-emoji=\"two_men_holding_hands\" data-unicode-name=\"1F46C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46D\" title=\"two_women_holding_hands\" data-aliases=\"\" data-emoji=\"two_women_holding_hands\" data-unicode-name=\"1F46D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F612\" title=\"unamused\" data-aliases=\"\" data-emoji=\"unamused\" data-unicode-name=\"1F612\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270C\" title=\"v\" data-aliases=\"\" data-emoji=\"v\" data-unicode-name=\"270C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F6B6\" title=\"walking\" data-aliases=\"\" data-emoji=\"walking\" data-unicode-name=\"1F6B6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44B\" title=\"wave\" data-aliases=\"\" data-emoji=\"wave\" data-unicode-name=\"1F44B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F629\" title=\"weary\" data-aliases=\"\" data-emoji=\"weary\" data-unicode-name=\"1F629\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F609\" title=\"wink\" data-aliases=\"\" data-emoji=\"wink\" data-unicode-name=\"1F609\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F469\" title=\"woman\" data-aliases=\"\" data-emoji=\"woman\" data-unicode-name=\"1F469\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45A\" title=\"womans_clothes\" data-aliases=\"\" data-emoji=\"womans_clothes\" data-unicode-name=\"1F45A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F452\" title=\"womans_hat\" data-aliases=\"\" data-emoji=\"womans_hat\" data-unicode-name=\"1F452\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61F\" title=\"worried\" data-aliases=\"\" data-emoji=\"worried\" data-unicode-name=\"1F61F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49B\" title=\"yellow_heart\" data-aliases=\"\" data-emoji=\"yellow_heart\" data-unicode-name=\"1F49B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60B\" title=\"yum\" data-aliases=\"\" data-emoji=\"yum\" data-unicode-name=\"1F60B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A4\" title=\"zzz\" data-aliases=\"\" data-emoji=\"zzz\" data-unicode-name=\"1F4A4\"></div>\n </button>\n </li>\n </ul>\n </div>\n</div>";
diff --git a/spec/javascripts/fixtures/environments/element.html.haml b/spec/javascripts/fixtures/environments/element.html.haml
new file mode 100644
index 00000000000..8d7aeb23356
--- /dev/null
+++ b/spec/javascripts/fixtures/environments/element.html.haml
@@ -0,0 +1 @@
+.test-dom-element
diff --git a/spec/javascripts/fixtures/environments/environments.html.haml b/spec/javascripts/fixtures/environments/environments.html.haml
new file mode 100644
index 00000000000..d89bc50c1f0
--- /dev/null
+++ b/spec/javascripts/fixtures/environments/environments.html.haml
@@ -0,0 +1,9 @@
+%div
+ #environments-list-view{ data: { environments_data: "https://gitlab.com/foo/environments",
+ "can-create-deployment" => "true",
+ "can-read-environment" => "true",
+ "can-create-environment" => "true",
+ "project-environments-path" => "https://gitlab.com/foo/environments",
+ "project-stopped-environments-path" => "https://gitlab.com/foo/environments?scope=stopped",
+ "new-environment-path" => "https://gitlab.com/foo/environments/new",
+ "help-page-path" => "https://gitlab.com/help_page"}}
diff --git a/spec/javascripts/fixtures/environments/table.html.haml b/spec/javascripts/fixtures/environments/table.html.haml
new file mode 100644
index 00000000000..1ea1725c561
--- /dev/null
+++ b/spec/javascripts/fixtures/environments/table.html.haml
@@ -0,0 +1,11 @@
+%table
+ %thead
+ %tr
+ %th Environment
+ %th Last deployment
+ %th Build
+ %th Commit
+ %th
+ %th
+ %tbody
+ %tr#environment-row
diff --git a/spec/javascripts/fixtures/event_filter.html.haml b/spec/javascripts/fixtures/event_filter.html.haml
index 95e248cadf8..5477c6075f0 100644
--- a/spec/javascripts/fixtures/event_filter.html.haml
+++ b/spec/javascripts/fixtures/event_filter.html.haml
@@ -12,6 +12,10 @@
%span
Merge events
%li
+ %a.event-filter-link{ id: "issue_event_filter", title: "Filter by issue events", href: "/dashboard/activity"}
+ %span
+ Issue events
+ %li
%a.event-filter-link{ id: "comments_event_filter", title: "Filter by comments", href: "/dashboard/activity"}
%span
Comments
diff --git a/spec/javascripts/fixtures/issuable.html.haml b/spec/javascripts/fixtures/issuable.html.haml
deleted file mode 100644
index 42ab4aa68b1..00000000000
--- a/spec/javascripts/fixtures/issuable.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-%form.js-main-target-form
- %textarea#note_note
diff --git a/spec/javascripts/fixtures/issue_note.html.haml b/spec/javascripts/fixtures/issue_note.html.haml
deleted file mode 100644
index 0aecc7334fd..00000000000
--- a/spec/javascripts/fixtures/issue_note.html.haml
+++ /dev/null
@@ -1,12 +0,0 @@
-%ul
- %li.note
- .js-task-list-container
- .note-text
- %ul.task-list
- %li.task-list-item
- %input.task-list-item-checkbox{type: 'checkbox'}
- Task List Item
- .note-edit-form
- %form
- %textarea.js-task-list-field
- \- [ ] Task List Item
diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb
index d95eb851421..06f708f9e15 100644
--- a/spec/javascripts/fixtures/issues.rb
+++ b/spec/javascripts/fixtures/issues.rb
@@ -4,7 +4,8 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller
include JavaScriptFixturesHelpers
let(:admin) { create(:admin) }
- let(:project) { create(:project_empty_repo) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project_empty_repo, namespace: namespace, path: 'issues-project') }
render_views
@@ -25,8 +26,13 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller
end
it 'issues/issue-with-task-list.html.raw' do |example|
+ issue = create(:issue, project: project, description: '- [ ] Task List Item')
+ render_issue(example.description, issue)
+ end
+
+ it 'issues/issue_with_comment.html.raw' do |example|
issue = create(:issue, project: project)
- issue.update(description: '- [ ] Task List Item')
+ create(:note, project: project, noteable: issue, note: '- [ ] Task List Item').save
render_issue(example.description, issue)
end
diff --git a/spec/javascripts/fixtures/linked_tabs.html.haml b/spec/javascripts/fixtures/linked_tabs.html.haml
new file mode 100644
index 00000000000..93c0cf97ff0
--- /dev/null
+++ b/spec/javascripts/fixtures/linked_tabs.html.haml
@@ -0,0 +1,13 @@
+%ul.nav.nav-tabs.linked-tabs
+ %li
+ %a{ href: 'foo/bar/1', data: { target: 'div#tab1', action: 'tab1', toggle: 'tab' } }
+ Tab 1
+ %li
+ %a{ href: 'foo/bar/1/context', data: { target: 'div#tab2', action: 'tab2', toggle: 'tab' } }
+ Tab 2
+
+.tab-content
+ #tab1.tab-pane
+ Tab 1 Content
+ #tab2.tab-pane
+ Tab 2 Content
diff --git a/spec/javascripts/fixtures/pipeline_graph.html.haml b/spec/javascripts/fixtures/pipeline_graph.html.haml
new file mode 100644
index 00000000000..deca50ceaa7
--- /dev/null
+++ b/spec/javascripts/fixtures/pipeline_graph.html.haml
@@ -0,0 +1,15 @@
+%div.pipeline-visualization.js-pipeline-graph
+ %ul.stage-column-list
+ %li.stage-column
+ .stage-name
+ %a{:href => "/"}
+ Test
+ .builds-container
+ %ul
+ %li.build
+ .curve
+ .build-content
+ %a
+ %svg
+ .ci-status-text
+ stop_review
diff --git a/spec/javascripts/fixtures/right_sidebar.html.haml b/spec/javascripts/fixtures/right_sidebar.html.haml
deleted file mode 100644
index d48b77cf0ce..00000000000
--- a/spec/javascripts/fixtures/right_sidebar.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-%div
- %div.page-gutter.page-with-sidebar
-
- %aside.right-sidebar
- %div.block.issuable-sidebar-header
- %a.gutter-toggle.pull-right.js-sidebar-toggle
- %i.fa.fa-angle-double-left
- %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", data: { todo_text: "Add Todo", mark_text: "Mark Done", issuable_id: "1", issuable_type: "issue", url: "/todos" }}
- %span.js-issuable-todo-text
- Add Todo
- %i.fa.fa-spin.fa-spinner.js-issuable-todo-loading.hidden
-
- %form.issuable-context-form
- %div.block.labels
- %div.sidebar-collapsed-icon
- %i.fa.fa-tags
- %span 1
diff --git a/spec/javascripts/fixtures/signin_tabs.html.haml b/spec/javascripts/fixtures/signin_tabs.html.haml
new file mode 100644
index 00000000000..12b8d423cbe
--- /dev/null
+++ b/spec/javascripts/fixtures/signin_tabs.html.haml
@@ -0,0 +1,5 @@
+%ul.nav-tabs
+ %li
+ %a.active{ id: 'standard', href: '#standard'} Standard
+ %li
+ %a{ id: 'ldap', href: '#ldap'} Ldap
diff --git a/spec/javascripts/fixtures/zen_mode.html.haml b/spec/javascripts/fixtures/zen_mode.html.haml
deleted file mode 100644
index cb906a7feaa..00000000000
--- a/spec/javascripts/fixtures/zen_mode.html.haml
+++ /dev/null
@@ -1,8 +0,0 @@
-.md-area
- .zen-backdrop
- %textarea#note_note.js-gfm-input.markdown-area
- %a.js-zen-enter(tabindex="-1" href="#")
- %i.fa.fa-expand
- Edit in fullscreen
- %a.js-zen-leave(tabindex="-1" href="#")
- %i.fa.fa-compress
diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6
index 8ba238018cd..bfaf90e2aee 100644
--- a/spec/javascripts/gl_dropdown_spec.js.es6
+++ b/spec/javascripts/gl_dropdown_spec.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, prefer-const, no-param-reassign, no-plusplus, semi, no-unused-expressions, arrow-spacing, max-len */
+/* global Turbolinks */
+
/*= require jquery */
/*= require gl_dropdown */
/*= require turbolinks */
diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6
index 0713e30e485..5018e87ad6c 100644
--- a/spec/javascripts/gl_field_errors_spec.js.es6
+++ b/spec/javascripts/gl_field_errors_spec.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, arrow-body-style, indent, padded-blocks */
+
//= require jquery
//= require gl_field_errors
diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
index 8c66c45ba79..bc5cbeb6a40 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable quotes, indent, semi, object-curly-spacing, jasmine/no-suite-dupes, vars-on-top, no-var, padded-blocks, spaced-comment, max-len */
+/* global d3 */
+/* global ContributorsGraph */
+/* global ContributorsMasterGraph */
+
//= require graphs/stat_graph_contributors_graph
describe("ContributorsGraph", function () {
diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
index 920e4ee0892..751f3d175e2 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces */
+/* global ContributorsStatGraphUtil */
+
//= require graphs/stat_graph_contributors_util
describe("ContributorsStatGraphUtil", function () {
diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js
index ae2821ecad9..0da124632ae 100644
--- a/spec/javascripts/graphs/stat_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_spec.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable quotes, padded-blocks, semi */
+/* global StatGraph */
+
//= require graphs/stat_graph
describe("StatGraph", function () {
diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js
index 9a859655d8b..d2bcbc37b64 100644
--- a/spec/javascripts/header_spec.js
+++ b/spec/javascripts/header_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, padded-blocks, no-var */
/*= require header */
/*= require lib/utils/text_utility */
/*= require jquery */
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 949114185cf..faab5ae00c2 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, indent, no-trailing-spaces, comma-dangle, padded-blocks, max-len */
+/* global Issue */
/*= require lib/utils/text_utility */
/*= require issue */
@@ -70,11 +71,11 @@
$('input[type=checkbox]').attr('checked', true).trigger('change');
expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
});
-
+
it('submits an ajax request on tasklist:changed', function() {
spyOn(jQuery, 'ajax').and.callFake(function(req) {
expect(req.type).toBe('PATCH');
- expect(req.url).toBe('https://fixture.invalid/namespace3/project3/issues/1.json');
+ expect(req.url).toBe(gl.TEST_HOST + '/frontend-fixtures/issues-project/issues/1.json'); // eslint-disable-line prefer-template
expect(req.data.issue.description).not.toBe(null);
});
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6
index 49687048eb5..0c48d04776f 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js.es6
+++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable no-new, no-plusplus, object-curly-spacing, prefer-const, semi */
+/* global IssuableContext */
+/* global LabelsSelect */
+
//= require lib/utils/type_utility
//= require jquery
//= require bootstrap
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6
new file mode 100644
index 00000000000..ef75f600898
--- /dev/null
+++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6
@@ -0,0 +1,32 @@
+//= require lib/utils/common_utils
+
+(() => {
+ describe('common_utils', () => {
+ describe('gl.utils.parseUrl', () => {
+ it('returns an anchor tag with url', () => {
+ expect(gl.utils.parseUrl('/some/absolute/url').pathname).toContain('some/absolute/url');
+ });
+ it('url is escaped', () => {
+ // 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');
+ });
+ });
+ describe('gl.utils.parseUrlPathname', () => {
+ beforeEach(() => {
+ spyOn(gl.utils, 'parseUrl').and.callFake(url => ({
+ pathname: url,
+ }));
+ });
+ it('returns an absolute url when given an absolute url', () => {
+ expect(gl.utils.parseUrlPathname('/some/absolute/url')).toEqual('/some/absolute/url');
+ });
+ it('returns an absolute url when given a relative url', () => {
+ expect(gl.utils.parseUrlPathname('some/relative/url')).toEqual('/some/relative/url');
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 b/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6
new file mode 100644
index 00000000000..3645dd70c55
--- /dev/null
+++ b/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6
@@ -0,0 +1,43 @@
+//= require lib/utils/custom_event_polyfill
+
+describe('Custom Event Polyfill', () => {
+ it('should be defined', () => {
+ expect(window.CustomEvent).toBeDefined();
+ });
+
+ it('should create a `CustomEvent` instance', () => {
+ const e = new window.CustomEvent('foo');
+
+ expect(e.type).toEqual('foo');
+ expect(e.bubbles).toBe(false);
+ expect(e.cancelable).toBe(false);
+ expect(e.detail).toBeFalsy();
+ });
+
+ it('should create a `CustomEvent` instance with a `details` object', () => {
+ const e = new window.CustomEvent('bar', { detail: { foo: 'bar' } });
+
+ expect(e.type).toEqual('bar');
+ expect(e.bubbles).toBe(false);
+ expect(e.cancelable).toBe(false);
+ expect(e.detail.foo).toEqual('bar');
+ });
+
+ it('should create a `CustomEvent` instance with a `bubbles` boolean', () => {
+ const e = new window.CustomEvent('bar', { bubbles: true });
+
+ expect(e.type).toEqual('bar');
+ expect(e.bubbles).toBe(true);
+ expect(e.cancelable).toBe(false);
+ expect(e.detail).toBeFalsy();
+ });
+
+ it('should create a `CustomEvent` instance with a `cancelable` boolean', () => {
+ const e = new window.CustomEvent('bar', { cancelable: true });
+
+ expect(e.type).toEqual('bar');
+ expect(e.bubbles).toBe(false);
+ expect(e.cancelable).toBe(true);
+ expect(e.detail).toBeFalsy();
+ });
+});
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index e0192a2d624..decdf583410 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-plusplus, jasmine/no-spec-dupes, no-underscore-dangle, padded-blocks, max-len */
+/* global LineHighlighter */
/*= require line_highlighter */
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index 83d279ab414..4cf1693af1b 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-return-assign, padded-blocks */
+/* global MergeRequest */
/*= require merge_request */
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 6a53c6aa6ac..130d391bfab 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -1,108 +1,122 @@
-/* eslint-disable */
+/* eslint-disable no-var, comma-dangle, object-shorthand */
/*= require merge_request_tabs */
//= require breakpoints
+//= require lib/utils/common_utils
+//= require jquery.scrollTo
-(function() {
- describe('MergeRequestTabs', function() {
- var stubLocation;
- stubLocation = function(stubs) {
- var defaults;
- defaults = {
+(function () {
+ describe('MergeRequestTabs', function () {
+ var stubLocation = {};
+ var setLocation = function (stubs) {
+ var defaults = {
pathname: '',
search: '',
hash: ''
};
- return $.extend(defaults, stubs);
+ $.extend(stubLocation, defaults, stubs || {});
};
fixture.preload('merge_request_tabs.html');
- beforeEach(function() {
- this["class"] = new MergeRequestTabs();
- return this.spies = {
- ajax: spyOn($, 'ajax').and.callFake(function() {}),
- history: spyOn(history, 'replaceState').and.callFake(function() {})
+
+ beforeEach(function () {
+ this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation });
+ setLocation();
+
+ this.spies = {
+ history: spyOn(window.history, 'replaceState').and.callFake(function () {})
};
});
- describe('#activateTab', function() {
- beforeEach(function() {
+
+ describe('#activateTab', function () {
+ beforeEach(function () {
+ spyOn($, 'ajax').and.callFake(function () {});
fixture.load('merge_request_tabs.html');
- return this.subject = this["class"].activateTab;
+ this.subject = this.class.activateTab;
});
- it('shows the first tab when action is show', function() {
+ it('shows the first tab when action is show', function () {
this.subject('show');
- return expect($('#notes')).toHaveClass('active');
+ expect($('#notes')).toHaveClass('active');
});
- it('shows the notes tab when action is notes', function() {
+ it('shows the notes tab when action is notes', function () {
this.subject('notes');
- return expect($('#notes')).toHaveClass('active');
+ expect($('#notes')).toHaveClass('active');
});
- it('shows the commits tab when action is commits', function() {
+ it('shows the commits tab when action is commits', function () {
this.subject('commits');
- return expect($('#commits')).toHaveClass('active');
+ expect($('#commits')).toHaveClass('active');
});
- return it('shows the diffs tab when action is diffs', function() {
+ it('shows the diffs tab when action is diffs', function () {
this.subject('diffs');
- return expect($('#diffs')).toHaveClass('active');
+ expect($('#diffs')).toHaveClass('active');
});
});
- return describe('#setCurrentAction', function() {
- beforeEach(function() {
- return this.subject = this["class"].setCurrentAction;
+
+ describe('#setCurrentAction', function () {
+ beforeEach(function () {
+ spyOn($, 'ajax').and.callFake(function () {});
+ this.subject = this.class.setCurrentAction;
});
- it('changes from commits', function() {
- this["class"]._location = stubLocation({
+ it('changes from commits', function () {
+ setLocation({
pathname: '/foo/bar/merge_requests/1/commits'
});
expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1');
- return expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs');
+ expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs');
});
- it('changes from diffs', function() {
- this["class"]._location = stubLocation({
+ it('changes from diffs', function () {
+ setLocation({
pathname: '/foo/bar/merge_requests/1/diffs'
});
expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1');
- return expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
+ expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
});
- it('changes from diffs.html', function() {
- this["class"]._location = stubLocation({
+ it('changes from diffs.html', function () {
+ setLocation({
pathname: '/foo/bar/merge_requests/1/diffs.html'
});
expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1');
- return expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
+ expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
});
- it('changes from notes', function() {
- this["class"]._location = stubLocation({
+ it('changes from notes', function () {
+ setLocation({
pathname: '/foo/bar/merge_requests/1'
});
expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs');
- return expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
+ expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits');
});
- it('includes search parameters and hash string', function() {
- this["class"]._location = stubLocation({
+ it('includes search parameters and hash string', function () {
+ setLocation({
pathname: '/foo/bar/merge_requests/1/diffs',
search: '?view=parallel',
hash: '#L15-35'
});
- return expect(this.subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35');
+ expect(this.subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35');
});
- it('replaces the current history state', function() {
- var new_state;
- this["class"]._location = stubLocation({
+ it('replaces the current history state', function () {
+ var newState;
+ setLocation({
pathname: '/foo/bar/merge_requests/1'
});
- new_state = this.subject('commits');
- return expect(this.spies.history).toHaveBeenCalledWith({
+ newState = this.subject('commits');
+ expect(this.spies.history).toHaveBeenCalledWith({
turbolinks: true,
- url: new_state
- }, document.title, new_state);
+ url: newState
+ }, document.title, newState);
});
- return it('treats "show" like "notes"', function() {
- this["class"]._location = stubLocation({
+ it('treats "show" like "notes"', function () {
+ setLocation({
pathname: '/foo/bar/merge_requests/1/commits'
});
- return expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
+ expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
+ });
+ });
+ describe('#loadDiff', function () {
+ it('requires an absolute pathname', function () {
+ spyOn($, 'ajax').and.callFake(function (options) {
+ expect(options.url).toEqual('/foo/bar/merge_requests/1/diffs.json');
+ });
+ this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js
index 49dfeab61d8..6f91529db00 100644
--- a/spec/javascripts/merge_request_widget_spec.js
+++ b/spec/javascripts/merge_request_widget_spec.js
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, indent, quote-props, no-var, padded-blocks, max-len */
+
/*= require merge_request_widget */
-/*= require jquery.timeago.js */
+/*= require lib/utils/datetime_utility */
(function() {
describe('MergeRequestWidget', function() {
@@ -35,9 +36,9 @@
external_url_formatted: 'test-url.com'
}];
- spyOn(jQuery, 'getJSON').and.callFake((req, cb) => {
+ spyOn(jQuery, 'getJSON').and.callFake(function(req, cb) {
cb(this.ciEnvironmentsStatusData);
- });
+ }.bind(this));
});
it('should call renderEnvironments when the environments property is set', function() {
@@ -54,6 +55,69 @@
});
});
+ describe('renderEnvironments', function() {
+ describe('should render correct timeago', function() {
+ beforeEach(function() {
+ this.environments = [{
+ id: 'test-environment-id',
+ url: 'testurl',
+ deployed_at: new Date().toISOString(),
+ deployed_at_formatted: true
+ }];
+ });
+
+ function getTimeagoText(template) {
+ var el = document.createElement('html');
+ el.innerHTML = template;
+ return el.querySelector('.js-environment-timeago').innerText.trim();
+ }
+
+ it('should render less than a minute ago text', function() {
+ spyOn(this.class.$widgetBody, 'before').and.callFake(function(template) {
+ expect(getTimeagoText(template)).toBe('less than a minute ago.');
+ });
+
+ this.class.renderEnvironments(this.environments);
+ });
+
+ it('should render about an hour ago text', function() {
+ var oneHourAgo = new Date();
+ oneHourAgo.setHours(oneHourAgo.getHours() - 1);
+
+ this.environments[0].deployed_at = oneHourAgo.toISOString();
+ spyOn(this.class.$widgetBody, 'before').and.callFake(function(template) {
+ expect(getTimeagoText(template)).toBe('about an hour ago.');
+ });
+
+ this.class.renderEnvironments(this.environments);
+ });
+
+ it('should render about 2 hours ago text', function() {
+ var twoHoursAgo = new Date();
+ twoHoursAgo.setHours(twoHoursAgo.getHours() - 2);
+
+ this.environments[0].deployed_at = twoHoursAgo.toISOString();
+ spyOn(this.class.$widgetBody, 'before').and.callFake(function(template) {
+ expect(getTimeagoText(template)).toBe('about 2 hours ago.');
+ });
+
+ this.class.renderEnvironments(this.environments);
+ });
+ });
+ });
+
+ describe('mergeInProgress', function() {
+ it('should display error with h4 tag', function() {
+ spyOn(this.class.$widgetBody, 'html').and.callFake(function(html) {
+ expect(html).toBe('<h4>Sorry, something went wrong.</h4>');
+ });
+ spyOn($, 'ajax').and.callFake(function(e) {
+ e.success({ merge_error: 'Sorry, something went wrong.' });
+ });
+ this.class.mergeInProgress(null);
+ });
+ });
+
return describe('getCIStatus', function() {
beforeEach(function() {
this.ciStatusData = {
diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js
index c092424ec32..a6cb9e47744 100644
--- a/spec/javascripts/new_branch_spec.js
+++ b/spec/javascripts/new_branch_spec.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, padded-blocks, max-len */
+/* global NewBranchForm */
/*= require jquery-ui/autocomplete */
/*= require new_branch_form */
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 2e3a4b66e2d..d3bfa7730fa 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */
+/* global Notes */
+
/*= require notes */
/*= require autosize */
/*= require gl_form */
@@ -6,17 +8,21 @@
(function() {
window.gon || (window.gon = {});
-
- window.disableButtonIfEmptyField = function() {
- return null;
- };
+ window.gl = window.gl || {};
+ gl.utils = gl.utils || {};
describe('Notes', function() {
- describe('task lists', function() {
- fixture.preload('issue_note.html');
+ var commentsTemplate = 'issues/issue_with_comment.raw';
+ fixture.preload(commentsTemplate);
+ beforeEach(function () {
+ fixture.load(commentsTemplate);
+ gl.utils.disableButtonIfEmptyField = _.noop;
+ window.project_uploads_path = 'http://test.host/uploads';
+ });
+
+ describe('task lists', function() {
beforeEach(function() {
- fixture.load('issue_note.html');
$('form').on('submit', function(e) {
e.preventDefault();
});
@@ -41,12 +47,9 @@
});
describe('comments', function() {
- var commentsTemplate = 'comments.html';
var textarea = '.js-note-text';
- fixture.preload(commentsTemplate);
beforeEach(function() {
- fixture.load(commentsTemplate);
this.notes = new Notes();
this.autoSizeSpy = spyOnEvent($(textarea), 'autosize:update');
diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js.es6
new file mode 100644
index 00000000000..85c9cf4b4f1
--- /dev/null
+++ b/spec/javascripts/pipelines_spec.js.es6
@@ -0,0 +1,25 @@
+//= require pipelines
+
+(() => {
+ describe('Pipelines', () => {
+ fixture.preload('pipeline_graph');
+
+ beforeEach(() => {
+ fixture.load('pipeline_graph');
+ });
+
+ it('should be defined', () => {
+ expect(window.gl.Pipelines).toBeDefined();
+ });
+
+ it('should create a `Pipelines` instance without options', () => {
+ expect(() => { new window.gl.Pipelines(); }).not.toThrow(); //eslint-disable-line
+ });
+
+ it('should create a `Pipelines` instance with options', () => {
+ const pipelines = new window.gl.Pipelines({ foo: 'bar' });
+
+ expect(pipelines.pipelineGraph).toBeDefined();
+ });
+ });
+})();
diff --git a/spec/javascripts/pretty_time_spec.js.es6 b/spec/javascripts/pretty_time_spec.js.es6
new file mode 100644
index 00000000000..2e12d45f7a7
--- /dev/null
+++ b/spec/javascripts/pretty_time_spec.js.es6
@@ -0,0 +1,134 @@
+//= require lib/utils/pretty_time
+
+(() => {
+ const PrettyTime = gl.PrettyTime;
+
+ describe('PrettyTime methods', function () {
+ describe('parseSeconds', function () {
+ it('should correctly parse a negative value', function () {
+ const parser = PrettyTime.parseSeconds;
+
+ const zeroSeconds = parser(-1000);
+
+ expect(zeroSeconds.minutes).toBe(16);
+ expect(zeroSeconds.hours).toBe(0);
+ expect(zeroSeconds.days).toBe(0);
+ expect(zeroSeconds.weeks).toBe(0);
+ });
+
+ it('should correctly parse a zero value', function () {
+ const parser = PrettyTime.parseSeconds;
+
+ const zeroSeconds = parser(0);
+
+ expect(zeroSeconds.minutes).toBe(0);
+ expect(zeroSeconds.hours).toBe(0);
+ expect(zeroSeconds.days).toBe(0);
+ expect(zeroSeconds.weeks).toBe(0);
+ });
+
+ it('should correctly parse a small non-zero second values', function () {
+ const parser = PrettyTime.parseSeconds;
+
+ const subOneMinute = parser(10);
+
+ expect(subOneMinute.minutes).toBe(0);
+ expect(subOneMinute.hours).toBe(0);
+ expect(subOneMinute.days).toBe(0);
+ expect(subOneMinute.weeks).toBe(0);
+
+ const aboveOneMinute = parser(100);
+
+ expect(aboveOneMinute.minutes).toBe(1);
+ expect(aboveOneMinute.hours).toBe(0);
+ expect(aboveOneMinute.days).toBe(0);
+ expect(aboveOneMinute.weeks).toBe(0);
+
+ const manyMinutes = parser(1000);
+
+ expect(manyMinutes.minutes).toBe(16);
+ expect(manyMinutes.hours).toBe(0);
+ expect(manyMinutes.days).toBe(0);
+ expect(manyMinutes.weeks).toBe(0);
+ });
+
+ it('should correctly parse large second values', function () {
+ const parser = PrettyTime.parseSeconds;
+
+ const aboveOneHour = parser(4800);
+
+ expect(aboveOneHour.minutes).toBe(20);
+ expect(aboveOneHour.hours).toBe(1);
+ expect(aboveOneHour.days).toBe(0);
+ expect(aboveOneHour.weeks).toBe(0);
+
+ const aboveOneDay = parser(110000);
+
+ expect(aboveOneDay.minutes).toBe(33);
+ expect(aboveOneDay.hours).toBe(6);
+ expect(aboveOneDay.days).toBe(3);
+ expect(aboveOneDay.weeks).toBe(0);
+
+ const aboveOneWeek = parser(25000000);
+
+ expect(aboveOneWeek.minutes).toBe(26);
+ expect(aboveOneWeek.hours).toBe(0);
+ expect(aboveOneWeek.days).toBe(3);
+ expect(aboveOneWeek.weeks).toBe(173);
+ });
+ });
+
+ describe('stringifyTime', function () {
+ it('should stringify values with all non-zero units', function () {
+ const timeObject = {
+ weeks: 1,
+ days: 4,
+ hours: 7,
+ minutes: 20,
+ };
+
+ const timeString = PrettyTime.stringifyTime(timeObject);
+
+ expect(timeString).toBe('1w 4d 7h 20m');
+ });
+
+ it('should stringify values with some non-zero units', function () {
+ const timeObject = {
+ weeks: 0,
+ days: 4,
+ hours: 0,
+ minutes: 20,
+ };
+
+ const timeString = PrettyTime.stringifyTime(timeObject);
+
+ expect(timeString).toBe('4d 20m');
+ });
+
+ it('should stringify values with no non-zero units', function () {
+ const timeObject = {
+ weeks: 0,
+ days: 0,
+ hours: 0,
+ minutes: 0,
+ };
+
+ const timeString = PrettyTime.stringifyTime(timeObject);
+
+ expect(timeString).toBe('0m');
+ });
+ });
+
+ describe('abbreviateTime', function () {
+ it('should abbreviate stringified times for weeks', function () {
+ const fullTimeString = '1w 3d 4h 5m';
+ expect(PrettyTime.abbreviateTime(fullTimeString)).toBe('1w');
+ });
+
+ it('should abbreviate stringified times for non-weeks', function () {
+ const fullTimeString = '0w 3d 4h 5m';
+ expect(PrettyTime.abbreviateTime(fullTimeString)).toBe('3d');
+ });
+ });
+ });
+})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
index 1963857bba3..bb802a4b5e3 100644
--- a/spec/javascripts/project_title_spec.js
+++ b/spec/javascripts/project_title_spec.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, padded-blocks, max-len */
+
+/* global Project */
/*= require bootstrap */
/*= require select2 */
@@ -21,16 +23,18 @@
return this.project = new Project();
});
return describe('project list', function() {
+ var fakeAjaxResponse = function fakeAjaxResponse(req) {
+ var d;
+ expect(req.url).toBe('/api/v3/projects.json?simple=true');
+ d = $.Deferred();
+ d.resolve(this.projects_data);
+ return d.promise();
+ };
+
beforeEach((function(_this) {
return function() {
_this.projects_data = fixture.load('projects.json')[0];
- return spyOn(jQuery, 'ajax').and.callFake(function(req) {
- var d;
- expect(req.url).toBe('/api/v3/projects.json?simple=true');
- d = $.Deferred();
- d.resolve(_this.projects_data);
- return d.promise();
- });
+ return spyOn(jQuery, 'ajax').and.callFake(fakeAjaxResponse.bind(_this));
};
})(this));
it('to show on toggle click', (function(_this) {
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index ef03d1147de..a083dbf033a 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, semi, padded-blocks, max-len */
+/* global Sidebar */
/*= require right_sidebar */
/*= require jquery */
@@ -34,9 +35,10 @@
};
describe('RightSidebar', function() {
- fixture.preload('right_sidebar.html');
+ var fixtureName = 'issues/open-issue.html.raw';
+ fixture.preload(fixtureName);
beforeEach(function() {
- fixture.load('right_sidebar.html');
+ fixture.load(fixtureName);
this.sidebar = new Sidebar;
$aside = $('.right-sidebar');
$page = $('.page-with-sidebar');
@@ -44,15 +46,12 @@
$toggle = $aside.find('.js-sidebar-toggle');
return $labelsIcon = $aside.find('.sidebar-collapsed-icon');
});
- it('should expand the sidebar when arrow is clicked', function() {
+ it('should expand/collapse the sidebar when arrow is clicked', function() {
+ assertSidebarState('expanded');
$toggle.click();
- return assertSidebarState('expanded');
- });
- it('should collapse the sidebar when arrow is clicked', function() {
+ assertSidebarState('collapsed');
$toggle.click();
assertSidebarState('expanded');
- $toggle.click();
- return assertSidebarState('collapsed');
});
it('should float over the page and when sidebar icons clicked', function() {
$labelsIcon.click();
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 29080804960..1b7f642d59e 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, padded-blocks, max-len */
/*= require gl_dropdown */
/*= require search_autocomplete */
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index 1f36a048153..7bc898aed5d 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -1,12 +1,15 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes, padded-blocks */
+/* global ShortcutsIssuable */
/*= require shortcuts_issuable */
(function() {
describe('ShortcutsIssuable', function() {
- fixture.preload('issuable.html');
+ var fixtureName = 'issues/open-issue.html.raw';
+ fixture.preload(fixtureName);
beforeEach(function() {
- fixture.load('issuable.html');
+ fixture.load(fixtureName);
+ document.querySelector('.js-new-note-form').classList.add('js-main-target-form');
return this.shortcut = new ShortcutsIssuable();
});
return describe('#replyWithSelectedText', function() {
diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 b/spec/javascripts/signin_tabs_memoizer_spec.js.es6
new file mode 100644
index 00000000000..9a9fb22255b
--- /dev/null
+++ b/spec/javascripts/signin_tabs_memoizer_spec.js.es6
@@ -0,0 +1,53 @@
+/*= require signin_tabs_memoizer */
+
+((global) => {
+ describe('SigninTabsMemoizer', () => {
+ const fixtureTemplate = 'signin_tabs.html';
+ const tabSelector = 'ul.nav-tabs';
+ const currentTabKey = 'current_signin_tab';
+ let memo;
+
+ function createMemoizer() {
+ memo = new global.ActiveTabMemoizer({
+ currentTabKey,
+ tabSelector,
+ });
+ return memo;
+ }
+
+ fixture.preload(fixtureTemplate);
+
+ beforeEach(() => {
+ fixture.load(fixtureTemplate);
+ });
+
+ it('does nothing if no tab was previously selected', () => {
+ createMemoizer();
+
+ expect(document.querySelector('li a.active').getAttribute('id')).toEqual('standard');
+ });
+
+ it('shows last selected tab on boot', () => {
+ createMemoizer().saveData('#ldap');
+ const fakeTab = {
+ click: () => {},
+ };
+ spyOn(document, 'querySelector').and.returnValue(fakeTab);
+ spyOn(fakeTab, 'click');
+
+ memo.bootstrap();
+
+ // verify that triggers click on the last selected tab
+ expect(document.querySelector).toHaveBeenCalledWith(`${tabSelector} a[href="#ldap"]`);
+ expect(fakeTab.click).toHaveBeenCalled();
+ });
+
+ it('saves last selected tab on change', () => {
+ createMemoizer();
+
+ document.getElementById('standard').click();
+
+ expect(memo.readData()).toEqual('#standard');
+ });
+ });
+})(window);
diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js.es6
new file mode 100644
index 00000000000..1b7ca97cde4
--- /dev/null
+++ b/spec/javascripts/smart_interval_spec.js.es6
@@ -0,0 +1,180 @@
+//= require jquery
+//= require smart_interval
+
+(() => {
+ const DEFAULT_MAX_INTERVAL = 100;
+ const DEFAULT_STARTING_INTERVAL = 5;
+ const DEFAULT_SHORT_TIMEOUT = 75;
+ const DEFAULT_LONG_TIMEOUT = 1000;
+ const DEFAULT_INCREMENT_FACTOR = 2;
+
+ function createDefaultSmartInterval(config) {
+ const defaultParams = {
+ callback: () => {},
+ startingInterval: DEFAULT_STARTING_INTERVAL,
+ maxInterval: DEFAULT_MAX_INTERVAL,
+ incrementByFactorOf: DEFAULT_INCREMENT_FACTOR,
+ lazyStart: false,
+ immediateExecution: false,
+ hiddenInterval: null,
+ };
+
+ if (config) {
+ _.extend(defaultParams, config);
+ }
+
+ return new gl.SmartInterval(defaultParams);
+ }
+
+ describe('SmartInterval', function () {
+ describe('Increment Interval', function () {
+ beforeEach(function () {
+ this.smartInterval = createDefaultSmartInterval();
+ });
+
+ it('should increment the interval delay', function (done) {
+ const interval = this.smartInterval;
+ setTimeout(() => {
+ const intervalConfig = this.smartInterval.cfg;
+ const iterationCount = 4;
+ const maxIntervalAfterIterations = intervalConfig.startingInterval *
+ (intervalConfig.incrementByFactorOf ** (iterationCount - 1)); // 40
+ const currentInterval = interval.getCurrentInterval();
+
+ // Provide some flexibility for performance of testing environment
+ expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval);
+ expect(currentInterval <= maxIntervalAfterIterations).toBeTruthy();
+
+ done();
+ }, DEFAULT_SHORT_TIMEOUT); // 4 iterations, increment by 2x = (5 + 10 + 20 + 40)
+ });
+
+ it('should not increment past maxInterval', function (done) {
+ const interval = this.smartInterval;
+
+ setTimeout(() => {
+ const currentInterval = interval.getCurrentInterval();
+ expect(currentInterval).toBe(interval.cfg.maxInterval);
+
+ done();
+ }, DEFAULT_LONG_TIMEOUT);
+ });
+ });
+
+ describe('Public methods', function () {
+ beforeEach(function () {
+ this.smartInterval = createDefaultSmartInterval();
+ });
+
+ it('should cancel an interval', function (done) {
+ const interval = this.smartInterval;
+
+ setTimeout(() => {
+ interval.cancel();
+
+ const intervalId = interval.state.intervalId;
+ const currentInterval = interval.getCurrentInterval();
+ const intervalLowerLimit = interval.cfg.startingInterval;
+
+ expect(intervalId).toBeUndefined();
+ expect(currentInterval).toBe(intervalLowerLimit);
+
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
+
+ it('should resume an interval', function (done) {
+ const interval = this.smartInterval;
+
+ setTimeout(() => {
+ interval.cancel();
+
+ interval.resume();
+
+ const intervalId = interval.state.intervalId;
+
+ expect(intervalId).toBeTruthy();
+
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
+ });
+
+ describe('DOM Events', function () {
+ beforeEach(function () {
+ // This ensures DOM and DOM events are initialized for these specs.
+ fixture.set('<div></div>');
+
+ this.smartInterval = createDefaultSmartInterval();
+ });
+
+ it('should pause when page is not visible', function (done) {
+ const interval = this.smartInterval;
+
+ setTimeout(() => {
+ expect(interval.state.intervalId).toBeTruthy();
+
+ // simulates triggering of visibilitychange event
+ interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
+
+ expect(interval.state.intervalId).toBeUndefined();
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
+
+ it('should change to the hidden interval when page is not visible', function (done) {
+ const HIDDEN_INTERVAL = 1500;
+ const interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL });
+
+ setTimeout(() => {
+ expect(interval.state.intervalId).toBeTruthy();
+ expect(interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL &&
+ interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL).toBeTruthy();
+
+ // simulates triggering of visibilitychange event
+ interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
+
+ expect(interval.state.intervalId).toBeTruthy();
+ expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL);
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
+
+ it('should resume when page is becomes visible at the previous interval', function (done) {
+ const interval = this.smartInterval;
+
+ setTimeout(() => {
+ expect(interval.state.intervalId).toBeTruthy();
+
+ // simulates triggering of visibilitychange event
+ interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
+
+ expect(interval.state.intervalId).toBeUndefined();
+
+ // simulates triggering of visibilitychange event
+ interval.handleVisibilityChange({ target: { visibilityState: 'visible' } });
+
+ expect(interval.state.intervalId).toBeTruthy();
+
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
+
+ it('should cancel on page unload', function (done) {
+ const interval = this.smartInterval;
+
+ setTimeout(() => {
+ $(document).trigger('page:before-unload');
+ expect(interval.state.intervalId).toBeUndefined();
+ expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
+
+ it('should execute callback before first interval', function () {
+ const interval = createDefaultSmartInterval({ immediateExecution: true });
+ expect(interval.cfg.immediateExecution).toBeFalsy();
+ });
+ });
+ });
+})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js
index 9cb8243ee2c..831dfada952 100644
--- a/spec/javascripts/spec_helper.js
+++ b/spec/javascripts/spec_helper.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren */
// PhantomJS (Teaspoons default driver) doesn't have support for
// Function.prototype.bind, which has caused confusion. Use this polyfill to
// avoid the confusion.
@@ -41,3 +41,8 @@
}).call(this);
+
+// 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 || {};
+gl.TEST_HOST = 'http://test.host';
diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js.es6
new file mode 100644
index 00000000000..6a70dd856a7
--- /dev/null
+++ b/spec/javascripts/subbable_resource_spec.js.es6
@@ -0,0 +1,66 @@
+/* eslint-disable max-len, arrow-parens, comma-dangle, no-plusplus */
+
+//= vue
+//= vue-resource
+//= require jquery
+//= require subbable_resource
+
+/*
+* Test that each rest verb calls the publish and subscribe function and passes the correct value back
+*
+*
+* */
+((global) => {
+ describe('Subbable Resource', function () {
+ describe('PubSub', function () {
+ beforeEach(function () {
+ this.MockResource = new global.SubbableResource('https://example.com');
+ });
+ it('should successfully add a single subscriber', function () {
+ const callback = () => {};
+ this.MockResource.subscribe(callback);
+
+ expect(this.MockResource.subscribers.length).toBe(1);
+ expect(this.MockResource.subscribers[0]).toBe(callback);
+ });
+
+ it('should successfully add multiple subscribers', function () {
+ const callbackOne = () => {};
+ const callbackTwo = () => {};
+ const callbackThree = () => {};
+
+ this.MockResource.subscribe(callbackOne);
+ this.MockResource.subscribe(callbackTwo);
+ this.MockResource.subscribe(callbackThree);
+
+ expect(this.MockResource.subscribers.length).toBe(3);
+ });
+
+ it('should successfully publish an update to a single subscriber', function () {
+ const state = { myprop: 1 };
+
+ const callbacks = {
+ one: (data) => expect(data.myprop).toBe(2),
+ two: (data) => expect(data.myprop).toBe(2),
+ three: (data) => expect(data.myprop).toBe(2)
+ };
+
+ const spyOne = spyOn(callbacks, 'one');
+ const spyTwo = spyOn(callbacks, 'two');
+ const spyThree = spyOn(callbacks, 'three');
+
+ this.MockResource.subscribe(callbacks.one);
+ this.MockResource.subscribe(callbacks.two);
+ this.MockResource.subscribe(callbacks.three);
+
+ state.myprop++;
+
+ this.MockResource.publish(state);
+
+ expect(spyOne).toHaveBeenCalled();
+ expect(spyTwo).toHaveBeenCalled();
+ expect(spyThree).toHaveBeenCalled();
+ });
+ });
+ });
+})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js
index 498f0f06797..ac411f6c306 100644
--- a/spec/javascripts/syntax_highlight_spec.js
+++ b/spec/javascripts/syntax_highlight_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes, padded-blocks */
/*= require syntax_highlight */
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index 024a91f0a80..a8874ab12d3 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, new-parens, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, padded-blocks, max-len */
+/* global MockU2FDevice */
+/* global U2FAuthenticate */
/*= require u2f/authenticate */
/*= require u2f/util */
diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js
index ad133682fb1..1459f968c3d 100644
--- a/spec/javascripts/u2f/mock_u2f_device.js
+++ b/spec/javascripts/u2f/mock_u2f_device.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-expressions, no-return-assign, no-param-reassign, padded-blocks, max-len */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
index abea76f622f..189592ea87a 100644
--- a/spec/javascripts/u2f/register_spec.js
+++ b/spec/javascripts/u2f/register_spec.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, new-parens, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */
+/* global MockU2FDevice */
+/* global U2FRegister */
/*= require u2f/register */
/*= require u2f/util */
diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6
new file mode 100644
index 00000000000..26dfdb94aae
--- /dev/null
+++ b/spec/javascripts/vue_common_components/commit_spec.js.es6
@@ -0,0 +1,131 @@
+//= require vue_common_component/commit
+
+describe('Commit component', () => {
+ let props;
+ let component;
+
+ it('should render a code-fork icon if it does not represent a tag', () => {
+ fixture.set('<div class="test-commit-container"></div>');
+ component = new window.gl.CommitComponent({
+ el: document.querySelector('.test-commit-container'),
+ propsData: {
+ tag: false,
+ commitRef: {
+ name: 'master',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ },
+ commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ shortSha: 'b7836edd',
+ title: 'Commit message',
+ author: {
+ avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
+ web_url: 'https://gitlab.com/jschatz1',
+ username: 'jschatz1',
+ },
+ },
+ });
+
+ expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-code-fork');
+ });
+
+ describe('Given all the props', () => {
+ beforeEach(() => {
+ fixture.set('<div class="test-commit-container"></div>');
+
+ props = {
+ tag: true,
+ commitRef: {
+ name: 'master',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ },
+ commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ shortSha: 'b7836edd',
+ title: 'Commit message',
+ author: {
+ avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
+ web_url: 'https://gitlab.com/jschatz1',
+ username: 'jschatz1',
+ },
+ commitIconSvg: '<svg></svg>',
+ };
+
+ component = new window.gl.CommitComponent({
+ el: document.querySelector('.test-commit-container'),
+ propsData: props,
+ });
+ });
+
+ it('should render a tag icon if it represents a tag', () => {
+ expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-tag');
+ });
+
+ it('should render a link to the ref url', () => {
+ expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.commitRef.ref_url);
+ });
+
+ it('should render the ref name', () => {
+ expect(component.$el.querySelector('.branch-name').textContent).toContain(props.commitRef.name);
+ });
+
+ it('should render the commit short sha with a link to the commit url', () => {
+ expect(component.$el.querySelector('.commit-id').getAttribute('href')).toEqual(props.commitUrl);
+ expect(component.$el.querySelector('.commit-id').textContent).toContain(props.shortSha);
+ });
+
+ it('should render the given commitIconSvg', () => {
+ expect(component.$el.querySelector('.js-commit-icon').children).toContain('svg');
+ });
+
+ describe('Given commit title and author props', () => {
+ it('should render a link to the author profile', () => {
+ expect(
+ component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href'),
+ ).toEqual(props.author.web_url);
+ });
+
+ it('Should render the author avatar with title and alt attributes', () => {
+ expect(
+ component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('title'),
+ ).toContain(props.author.username);
+ expect(
+ component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt'),
+ ).toContain(`${props.author.username}'s avatar`);
+ });
+ });
+
+ it('should render the commit title', () => {
+ expect(
+ component.$el.querySelector('a.commit-row-message').getAttribute('href'),
+ ).toEqual(props.commitUrl);
+ expect(
+ component.$el.querySelector('a.commit-row-message').textContent,
+ ).toContain(props.title);
+ });
+ });
+
+ describe('When commit title is not provided', () => {
+ it('should render default message', () => {
+ fixture.set('<div class="test-commit-container"></div>');
+ props = {
+ tag: false,
+ commitRef: {
+ name: 'master',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ },
+ commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ shortSha: 'b7836edd',
+ title: null,
+ author: {},
+ };
+
+ component = new window.gl.CommitComponent({
+ el: document.querySelector('.test-commit-container'),
+ propsData: props,
+ });
+
+ expect(
+ component.$el.querySelector('.commit-title span').textContent,
+ ).toContain('Cant find HEAD commit for this branch');
+ });
+ });
+});
diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
index 65b6e3dce33..d80ce5a7f7e 100644
--- a/spec/javascripts/zen_mode_spec.js
+++ b/spec/javascripts/zen_mode_spec.js
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-return-assign, new-cap, padded-blocks, max-len */
+/* global Dropzone */
+/* global Mousetrap */
+/* global ZenMode */
/*= require zen_mode */
@@ -6,9 +9,10 @@
var enterZen, escapeKeydown, exitZen;
describe('ZenMode', function() {
- fixture.preload('zen_mode.html');
+ var fixtureName = 'issues/open-issue.html.raw';
+ fixture.preload(fixtureName);
beforeEach(function() {
- fixture.load('zen_mode.html');
+ fixture.load(fixtureName);
spyOn(Dropzone, 'forElement').and.callFake(function() {
return {
enable: function() {
@@ -60,11 +64,11 @@
});
enterZen = function() {
- return $('a.js-zen-enter').click();
+ return $('.js-zen-enter').click();
};
exitZen = function() { // Ohmmmmmmm
- return $('a.js-zen-leave').click();
+ return $('.js-zen-leave').click();
};
escapeKeydown = function() {
diff --git a/spec/lib/banzai/filter/abstract_link_filter_spec.rb b/spec/lib/banzai/filter/abstract_link_filter_spec.rb
index 1ee31a603e4..70a87fbc01e 100644
--- a/spec/lib/banzai/filter/abstract_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/abstract_link_filter_spec.rb
@@ -5,7 +5,7 @@ describe Banzai::Filter::AbstractReferenceFilter do
describe '#references_per_project' do
it 'returns a Hash containing references grouped per project paths' do
- doc = Nokogiri::HTML.fragment("#1 #{project.to_reference}#2")
+ doc = Nokogiri::HTML.fragment("#1 #{project.path_with_namespace}#2")
filter = described_class.new(doc, project: project)
expect(filter).to receive(:object_class).exactly(4).times.and_return(Issue)
@@ -14,7 +14,7 @@ describe Banzai::Filter::AbstractReferenceFilter do
refs = filter.references_per_project
expect(refs).to be_an_instance_of(Hash)
- expect(refs[project.to_reference]).to eq(Set.new(%w[1 2]))
+ expect(refs[project.path_with_namespace]).to eq(Set.new(%w[1 2]))
end
end
diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb
index dca7f997570..a6d2ea11fcc 100644
--- a/spec/lib/banzai/filter/autolink_filter_spec.rb
+++ b/spec/lib/banzai/filter/autolink_filter_spec.rb
@@ -99,6 +99,28 @@ describe Banzai::Filter::AutolinkFilter, lib: true do
expect(doc.at_css('a')['href']).to eq link
end
+ it 'autolinks rdar' do
+ link = 'rdar://localhost.com/blah'
+ doc = filter("See #{link}")
+
+ expect(doc.at_css('a').text).to eq link
+ expect(doc.at_css('a')['href']).to eq link
+ end
+
+ it 'does not autolink javascript' do
+ link = 'javascript://alert(document.cookie);'
+ doc = filter("See #{link}")
+
+ expect(doc.at_css('a')).to be_nil
+ end
+
+ it 'does not autolink bad URLs' do
+ link = 'foo://23423:::asdf'
+ doc = filter("See #{link}")
+
+ expect(doc.to_s).to eq("See #{link}")
+ end
+
it 'does not include trailing punctuation' do
doc = filter("See #{link}.")
expect(doc.at_css('a').text).to eq link
diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
index e6c90ad87ee..9703e2315b8 100644
--- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
@@ -59,9 +59,7 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
it 'ignores invalid commit IDs' do
exp = act = "See #{commit1.id.reverse}...#{commit2.id}"
- expect(project).to receive(:valid_repo?).and_return(true)
- expect(project.repository).to receive(:commit).with(commit1.id.reverse)
- expect(project.repository).to receive(:commit).with(commit2.id)
+ allow(project.repository).to receive(:commit).with(commit1.id.reverse)
expect(reference_filter(act).to_html).to eq exp
end
@@ -100,14 +98,44 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
end
end
- context 'cross-project reference' do
- let(:namespace) { create(:namespace, name: 'cross-reference') }
- let(:project2) { create(:project, :public, namespace: namespace) }
- let(:reference) { range.to_reference(project) }
+ context 'cross-project / cross-namespace complete reference' do
+ let(:project2) { create(:project, :public) }
+ let(:reference) { "#{project2.path_with_namespace}@#{commit1.id}...#{commit2.id}" }
- before do
- range.project = project2
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param)
+ end
+
+ it 'link has valid text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ expect(doc.css('a').first.text).
+ to eql("#{project2.path_with_namespace}@#{commit1.short_id}...#{commit2.short_id}")
+ end
+
+ it 'has valid text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ expect(doc.text).to eql("Fixed (#{project2.path_with_namespace}@#{commit1.short_id}...#{commit2.short_id}.)")
+ end
+
+ it 'ignores invalid commit IDs on the referenced project' do
+ exp = act = "Fixed #{project2.path_with_namespace}@#{commit1.id.reverse}...#{commit2.id}"
+ expect(reference_filter(act).to_html).to eq exp
+
+ exp = act = "Fixed #{project2.path_with_namespace}@#{commit1.id}...#{commit2.id.reverse}"
+ expect(reference_filter(act).to_html).to eq exp
end
+ end
+
+ context 'cross-project / same-namespace complete reference' do
+ let(:namespace) { create(:namespace) }
+ let(:project) { create(:project, :public, namespace: namespace) }
+ let(:project2) { create(:project, :public, path: "same-namespace", namespace: namespace) }
+ let(:reference) { "#{project2.path}@#{commit1.id}...#{commit2.id}" }
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
@@ -116,24 +144,65 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param)
end
- it 'links with adjacent text' do
+ it 'link has valid text' do
doc = reference_filter("Fixed (#{reference}.)")
- exp = Regexp.escape("#{project2.to_reference}@#{range.reference_link_text}")
- expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
+ expect(doc.css('a').first.text).
+ to eql("#{project2.path}@#{commit1.short_id}...#{commit2.short_id}")
+ end
+
+ it 'has valid text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ expect(doc.text).to eql("Fixed (#{project2.path}@#{commit1.short_id}...#{commit2.short_id}.)")
end
it 'ignores invalid commit IDs on the referenced project' do
- exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}"
+ exp = act = "Fixed #{project2.path}@#{commit1.id.reverse}...#{commit2.id}"
expect(reference_filter(act).to_html).to eq exp
- exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
+ exp = act = "Fixed #{project2.path}@#{commit1.id}...#{commit2.id.reverse}"
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'cross-project shorthand reference' do
+ let(:namespace) { create(:namespace) }
+ let(:project) { create(:project, :public, namespace: namespace) }
+ let(:project2) { create(:project, :public, path: "same-namespace", namespace: namespace) }
+ let(:reference) { "#{project2.path}@#{commit1.id}...#{commit2.id}" }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param)
+ end
+
+ it 'link has valid text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ expect(doc.css('a').first.text).
+ to eql("#{project2.path}@#{commit1.short_id}...#{commit2.short_id}")
+ end
+
+ it 'has valid text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ expect(doc.text).to eql("Fixed (#{project2.path}@#{commit1.short_id}...#{commit2.short_id}.)")
+ end
+
+ it 'ignores invalid commit IDs on the referenced project' do
+ exp = act = "Fixed #{project2.path}@#{commit1.id.reverse}...#{commit2.id}"
+ expect(reference_filter(act).to_html).to eq exp
+
+ exp = act = "Fixed #{project2.path}@#{commit1.id}...#{commit2.id.reverse}"
expect(reference_filter(act).to_html).to eq exp
end
end
context 'cross-project URL reference' do
- let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:namespace) { create(:namespace) }
let(:project2) { create(:project, :public, namespace: namespace) }
let(:range) { CommitRange.new("#{commit1.id}...master", project) }
let(:reference) { urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: 'master') }
diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
index e0f08282551..2e6dcc3a434 100644
--- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
@@ -41,6 +41,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
it 'links with adjacent text' do
doc = reference_filter("See (#{reference}.)")
+
expect(doc.to_html).to match(/\(<a.+>#{commit.short_id}<\/a>\.\)/)
end
@@ -48,8 +49,6 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
invalid = invalidate_reference(reference)
exp = act = "See #{invalid}"
- expect(project).to receive(:valid_repo?).and_return(true)
- expect(project.repository).to receive(:commit).with(invalid)
expect(reference_filter(act).to_html).to eq exp
end
@@ -95,34 +94,85 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
end
end
- context 'cross-project reference' do
- let(:namespace) { create(:namespace, name: 'cross-reference') }
+ context 'cross-project / cross-namespace complete reference' do
+ let(:namespace) { create(:namespace) }
let(:project2) { create(:project, :public, namespace: namespace) }
let(:commit) { project2.commit }
- let(:reference) { commit.to_reference(project) }
+ let(:reference) { "#{project2.path_with_namespace}@#{commit.short_id}" }
- it 'links to a valid reference' do
- doc = reference_filter("See #{reference}")
+ it 'link has valid text' do
+ doc = reference_filter("See (#{reference}.)")
- expect(doc.css('a').first.attr('href')).
- to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id)
+ expect(doc.css('a').first.text).to eql("#{project2.path_with_namespace}@#{commit.short_id}")
end
- it 'links with adjacent text' do
- doc = reference_filter("Fixed (#{reference}.)")
+ it 'has valid text' do
+ doc = reference_filter("See (#{reference}.)")
+
+ expect(doc.text).to eql("See (#{project2.path_with_namespace}@#{commit.short_id}.)")
+ end
+
+ it 'ignores invalid commit IDs on the referenced project' do
+ exp = act = "Committed #{invalidate_reference(reference)}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'cross-project / same-namespace complete reference' do
+ let(:namespace) { create(:namespace) }
+ let(:project) { create(:empty_project, namespace: namespace) }
+ let(:project2) { create(:project, :public, namespace: namespace) }
+ let(:commit) { project2.commit }
+ let(:reference) { "#{project2.path_with_namespace}@#{commit.short_id}" }
+
+ it 'link has valid text' do
+ doc = reference_filter("See (#{reference}.)")
- exp = Regexp.escape(project2.to_reference)
- expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/)
+ expect(doc.css('a').first.text).to eql("#{project2.path}@#{commit.short_id}")
+ end
+
+ it 'has valid text' do
+ doc = reference_filter("See (#{reference}.)")
+
+ expect(doc.text).to eql("See (#{project2.path}@#{commit.short_id}.)")
end
it 'ignores invalid commit IDs on the referenced project' do
exp = act = "Committed #{invalidate_reference(reference)}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'cross-project shorthand reference' do
+ let(:namespace) { create(:namespace) }
+ let(:project) { create(:empty_project, namespace: namespace) }
+ let(:project2) { create(:project, :public, namespace: namespace) }
+ let(:commit) { project2.commit }
+ let(:reference) { "#{project2.path_with_namespace}@#{commit.short_id}" }
+
+ it 'link has valid text' do
+ doc = reference_filter("See (#{reference}.)")
+
+ expect(doc.css('a').first.text).to eql("#{project2.path}@#{commit.short_id}")
+ end
+
+ it 'has valid text' do
+ doc = reference_filter("See (#{reference}.)")
+
+ expect(doc.text).to eql("See (#{project2.path}@#{commit.short_id}.)")
+ end
+
+ it 'ignores invalid commit IDs on the referenced project' do
+ exp = act = "Committed #{invalidate_reference(reference)}"
+
expect(reference_filter(act).to_html).to eq exp
end
end
context 'cross-project URL reference' do
- let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:namespace) { create(:namespace) }
let(:project2) { create(:project, :public, namespace: namespace) }
let(:commit) { project2.commit }
let(:reference) { urls.namespace_project_commit_url(project2.namespace, project2, commit.id) }
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index 8f0b2db3e8e..456dbac0698 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -8,7 +8,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
end
let(:project) { create(:empty_project, :public) }
- let(:issue) { create(:issue, project: project) }
+ let(:issue) { create(:issue, project: project) }
it 'requires project context' do
expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
@@ -24,7 +24,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
context 'internal reference' do
it_behaves_like 'a reference containing an element node'
- let(:reference) { issue.to_reference }
+ let(:reference) { "##{issue.iid}" }
it 'ignores valid references when using non-default tracker' do
allow(project).to receive(:default_issues_tracker?).and_return(false)
@@ -42,7 +42,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
it 'links with adjacent text' do
doc = reference_filter("Fixed (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ expect(doc.text).to eql("Fixed (#{reference}.)")
end
it 'ignores invalid issue IDs' do
@@ -116,13 +116,56 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
end
end
- context 'cross-project reference' do
+ context 'cross-project / cross-namespace complete reference' do
it_behaves_like 'a reference containing an element node'
- let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:empty_project, :public) }
+ let(:issue) { create(:issue, project: project2) }
+ let(:reference) { "#{project2.path_with_namespace}##{issue.iid}" }
+
+ it 'ignores valid references when cross-reference project uses external tracker' do
+ expect_any_instance_of(described_class).to receive(:find_object).
+ with(project2, issue.iid).
+ and_return(nil)
+
+ exp = act = "Issue #{reference}"
+ expect(reference_filter(act).to_html).to eq exp
+ end
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq helper.url_for_issue(issue.iid, project2)
+ end
+
+ it 'link has valid text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ expect(doc.css('a').first.text).to eql("#{project2.path_with_namespace}##{issue.iid}")
+ end
+
+ it 'has valid text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ expect(doc.text).to eq("Fixed (#{project2.path_with_namespace}##{issue.iid}.)")
+ end
+
+ it 'ignores invalid issue IDs on the referenced project' do
+ exp = act = "Fixed #{invalidate_reference(reference)}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'cross-project / same-namespace complete reference' do
+ it_behaves_like 'a reference containing an element node'
+
+ let(:namespace) { create(:namespace) }
+ let(:project) { create(:empty_project, :public, namespace: namespace) }
let(:project2) { create(:empty_project, :public, namespace: namespace) }
let(:issue) { create(:issue, project: project2) }
- let(:reference) { issue.to_reference(project) }
+ let(:reference) { "#{project2.path_with_namespace}##{issue.iid}" }
it 'ignores valid references when cross-reference project uses external tracker' do
expect_any_instance_of(described_class).to receive(:find_object).
@@ -140,9 +183,16 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
to eq helper.url_for_issue(issue.iid, project2)
end
- it 'links with adjacent text' do
+ it 'link has valid text' do
doc = reference_filter("Fixed (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+
+ expect(doc.css('a').first.text).to eql("#{project2.path}##{issue.iid}")
+ end
+
+ it 'has valid text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ expect(doc.text).to eq("Fixed (#{project2.path}##{issue.iid}.)")
end
it 'ignores invalid issue IDs on the referenced project' do
@@ -150,9 +200,47 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to eq exp
end
+ end
+
+ context 'cross-project shorthand reference' do
+ it_behaves_like 'a reference containing an element node'
- it 'ignores out-of-bounds issue IDs on the referenced project' do
- exp = act = "Fixed ##{Gitlab::Database::MAX_INT_VALUE + 1}"
+ let(:namespace) { create(:namespace) }
+ let(:project) { create(:empty_project, :public, namespace: namespace) }
+ let(:project2) { create(:empty_project, :public, namespace: namespace) }
+ let(:issue) { create(:issue, project: project2) }
+ let(:reference) { "#{project2.path}##{issue.iid}" }
+
+ it 'ignores valid references when cross-reference project uses external tracker' do
+ expect_any_instance_of(described_class).to receive(:find_object).
+ with(project2, issue.iid).
+ and_return(nil)
+
+ exp = act = "Issue #{reference}"
+ expect(reference_filter(act).to_html).to eq exp
+ end
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq helper.url_for_issue(issue.iid, project2)
+ end
+
+ it 'link has valid text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ expect(doc.css('a').first.text).to eql("#{project2.path}##{issue.iid}")
+ end
+
+ it 'has valid text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ expect(doc.text).to eq("Fixed (#{project2.path}##{issue.iid}.)")
+ end
+
+ it 'ignores invalid issue IDs on the referenced project' do
+ exp = act = "Fixed #{invalidate_reference(reference)}"
expect(reference_filter(act).to_html).to eq exp
end
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index 9c09f00ae8a..284641fb20a 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -4,7 +4,7 @@ require 'html/pipeline'
describe Banzai::Filter::LabelReferenceFilter, lib: true do
include FilterSpecHelper
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:empty_project, :public, name: 'sample-project') }
let(:label) { create(:label, project: project) }
let(:reference) { label.to_reference }
@@ -48,6 +48,14 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
expect(link).to eq urls.namespace_project_issues_path(project.namespace, project, label_name: label.name)
end
+ context 'project that does not exist referenced' do
+ let(:result) { reference_filter('aaa/bbb~ccc') }
+
+ it 'does not link reference' do
+ expect(result.to_html).to eq 'aaa/bbb~ccc'
+ end
+ end
+
describe 'label span element' do
it 'includes default classes' do
doc = reference_filter("Label #{reference}")
@@ -334,14 +342,14 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end
context 'with project reference' do
- let(:reference) { project.to_reference + group_label.to_reference(format: :name) }
+ let(:reference) { "#{project.to_reference}#{group_label.to_reference(format: :name)}" }
it 'links to a valid reference' do
doc = reference_filter("See #{reference}", project: project)
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_issues_url(project.namespace, project, label_name: group_label.name)
- expect(doc.text).to eq 'See gfm references'
+ expect(doc.text).to eq "See gfm references"
end
it 'links with adjacent text' do
@@ -357,68 +365,247 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end
end
- describe 'cross project label references' do
- context 'valid project referenced' do
- let(:another_project) { create(:empty_project, :public) }
- let(:project_name) { another_project.name_with_namespace }
- let(:label) { create(:label, project: another_project, color: '#00ff00') }
- let(:reference) { label.to_reference(project) }
+ describe 'cross-project / cross-namespace complete reference' do
+ let(:project2) { create(:empty_project) }
+ let(:label) { create(:label, project: project2, color: '#00ff00') }
+ let(:reference) { "#{project2.path_with_namespace}~#{label.name}" }
+ let!(:result) { reference_filter("See #{reference}") }
- let!(:result) { reference_filter("See #{reference}") }
+ it 'links to a valid reference' do
+ expect(result.css('a').first.attr('href'))
+ .to eq urls.namespace_project_issues_url(project2.namespace,
+ project2,
+ label_name: label.name)
+ end
- it 'points to referenced project issues page' do
- expect(result.css('a').first.attr('href'))
- .to eq urls.namespace_project_issues_url(another_project.namespace,
- another_project,
- label_name: label.name)
- end
+ it 'has valid color' do
+ expect(result.css('a span').first.attr('style')).to match /background-color: #00ff00/
+ end
- it 'has valid color' do
- expect(result.css('a span').first.attr('style'))
- .to match /background-color: #00ff00/
- end
+ it 'has valid link text' do
+ expect(result.css('a').first.text).to eq "#{label.name} in #{project2.name_with_namespace}"
+ end
- it 'contains cross project content' do
- expect(result.css('a').first.text).to eq "#{label.name} in #{project_name}"
- end
+ it 'has valid text' do
+ expect(result.text).to eq "See #{label.name} in #{project2.name_with_namespace}"
end
- context 'project that does not exist referenced' do
- let(:result) { reference_filter('aaa/bbb~ccc') }
+ it 'ignores invalid IDs on the referenced label' do
+ exp = act = "See #{invalidate_reference(reference)}"
- it 'does not link reference' do
- expect(result.to_html).to eq 'aaa/bbb~ccc'
- end
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ describe 'cross-project / same-namespace complete reference' do
+ let(:namespace) { create(:namespace) }
+ let(:project) { create(:empty_project, namespace: namespace) }
+ let(:project2) { create(:empty_project, namespace: namespace) }
+ let(:label) { create(:label, project: project2, color: '#00ff00') }
+ let(:reference) { "#{project2.path_with_namespace}~#{label.name}" }
+ let!(:result) { reference_filter("See #{reference}") }
+
+ it 'links to a valid reference' do
+ expect(result.css('a').first.attr('href'))
+ .to eq urls.namespace_project_issues_url(project2.namespace,
+ project2,
+ label_name: label.name)
+ end
+
+ it 'has valid color' do
+ expect(result.css('a span').first.attr('style')).to match /background-color: #00ff00/
+ end
+
+ it 'has valid link text' do
+ expect(result.css('a').first.text).to eq "#{label.name} in #{project2.name}"
+ end
+
+ it 'has valid text' do
+ expect(result.text).to eq "See #{label.name} in #{project2.name}"
+ end
+
+ it 'ignores invalid IDs on the referenced label' do
+ exp = act = "See #{invalidate_reference(reference)}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ describe 'cross-project shorthand reference' do
+ let(:namespace) { create(:namespace) }
+ let(:project) { create(:empty_project, namespace: namespace) }
+ let(:project2) { create(:empty_project, namespace: namespace) }
+ let(:label) { create(:label, project: project2, color: '#00ff00') }
+ let(:reference) { "#{project2.path}~#{label.name}" }
+ let!(:result) { reference_filter("See #{reference}") }
+
+ it 'links to a valid reference' do
+ expect(result.css('a').first.attr('href'))
+ .to eq urls.namespace_project_issues_url(project2.namespace,
+ project2,
+ label_name: label.name)
+ end
+
+ it 'has valid color' do
+ expect(result.css('a span').first.attr('style')).
+ to match /background-color: #00ff00/
+ end
+
+ it 'has valid link text' do
+ expect(result.css('a').first.text).to eq "#{label.name} in #{project2.name}"
+ end
+
+ it 'has valid text' do
+ expect(result.text).to eq "See #{label.name} in #{project2.name}"
+ end
+
+ it 'ignores invalid IDs on the referenced label' do
+ exp = act = "See #{invalidate_reference(reference)}"
+
+ expect(reference_filter(act).to_html).to eq exp
end
end
describe 'cross group label references' do
- context 'valid project referenced' do
- let(:group) { create(:group) }
- let(:project) { create(:empty_project, :public, namespace: group) }
- let(:another_group) { create(:group) }
- let(:another_project) { create(:empty_project, :public, namespace: another_group) }
- let(:project_name) { another_project.name_with_namespace }
- let(:group_label) { create(:group_label, group: another_group, color: '#00ff00') }
- let(:reference) { another_project.to_reference + group_label.to_reference }
-
- let!(:result) { reference_filter("See #{reference}", project: project) }
-
- it 'points to referenced project issues page' do
- expect(result.css('a').first.attr('href'))
- .to eq urls.namespace_project_issues_url(another_project.namespace,
- another_project,
- label_name: group_label.name)
- end
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, :public, namespace: group) }
+ let(:another_group) { create(:group) }
+ let(:another_project) { create(:empty_project, :public, namespace: another_group) }
+ let(:group_label) { create(:group_label, group: another_group, color: '#00ff00') }
+ let(:reference) { "#{another_project.path_with_namespace}~#{group_label.name}" }
+ let!(:result) { reference_filter("See #{reference}", project: project) }
- it 'has valid color' do
- expect(result.css('a span').first.attr('style'))
- .to match /background-color: #00ff00/
- end
+ it 'points to referenced project issues page' do
+ expect(result.css('a').first.attr('href'))
+ .to eq urls.namespace_project_issues_url(another_project.namespace,
+ another_project,
+ label_name: group_label.name)
+ end
- it 'contains cross project content' do
- expect(result.css('a').first.text).to eq "#{group_label.name} in #{project_name}"
- end
+ it 'has valid color' do
+ expect(result.css('a span').first.attr('style')).
+ to match /background-color: #00ff00/
+ end
+
+ it 'has valid link text' do
+ expect(result.css('a').first.text).
+ to eq "#{group_label.name} in #{another_project.name_with_namespace}"
+ end
+
+ it 'has valid text' do
+ expect(result.text).
+ to eq "See #{group_label.name} in #{another_project.name_with_namespace}"
+ end
+
+ it 'ignores invalid IDs on the referenced label' do
+ exp = act = "See #{invalidate_reference(reference)}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ describe 'cross-project / same-group_label complete reference' do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, :public, namespace: group) }
+ let(:another_project) { create(:empty_project, :public, namespace: group) }
+ let(:group_label) { create(:group_label, group: group, color: '#00ff00') }
+ let(:reference) { "#{another_project.path_with_namespace}~#{group_label.name}" }
+ let!(:result) { reference_filter("See #{reference}", project: project) }
+
+ it 'points to referenced project issues page' do
+ expect(result.css('a').first.attr('href')).
+ to eq urls.namespace_project_issues_url(another_project.namespace,
+ another_project,
+ label_name: group_label.name)
+ end
+
+ it 'has valid color' do
+ expect(result.css('a span').first.attr('style')).
+ to match /background-color: #00ff00/
+ end
+
+ it 'has valid link text' do
+ expect(result.css('a').first.text).
+ to eq "#{group_label.name} in #{another_project.name}"
+ end
+
+ it 'has valid text' do
+ expect(result.text).
+ to eq "See #{group_label.name} in #{another_project.name}"
+ end
+
+ it 'ignores invalid IDs on the referenced label' do
+ exp = act = "See #{invalidate_reference(reference)}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ describe 'same project / same group_label complete reference' do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, :public, namespace: group) }
+ let(:group_label) { create(:group_label, group: group, color: '#00ff00') }
+ let(:reference) { "#{project.path_with_namespace}~#{group_label.name}" }
+ let!(:result) { reference_filter("See #{reference}", project: project) }
+
+ it 'points to referenced project issues page' do
+ expect(result.css('a').first.attr('href'))
+ .to eq urls.namespace_project_issues_url(project.namespace,
+ project,
+ label_name: group_label.name)
+ end
+
+ it 'has valid color' do
+ expect(result.css('a span').first.attr('style'))
+ .to match /background-color: #00ff00/
+ end
+
+ it 'has valid link text' do
+ expect(result.css('a').first.text).to eq group_label.name
+ end
+
+ it 'has valid text' do
+ expect(result.text).to eq "See #{group_label.name}"
+ end
+
+ it 'ignores invalid IDs on the referenced label' do
+ exp = act = "See #{invalidate_reference(reference)}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ describe 'same project / same group_label shorthand reference' do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, :public, namespace: group) }
+ let(:group_label) { create(:group_label, group: group, color: '#00ff00') }
+ let(:reference) { "#{project.path}~#{group_label.name}" }
+ let!(:result) { reference_filter("See #{reference}", project: project) }
+
+ it 'points to referenced project issues page' do
+ expect(result.css('a').first.attr('href'))
+ .to eq urls.namespace_project_issues_url(project.namespace,
+ project,
+ label_name: group_label.name)
+ end
+
+ it 'has valid color' do
+ expect(result.css('a span').first.attr('style')).
+ to match /background-color: #00ff00/
+ end
+
+ it 'has valid link text' do
+ expect(result.css('a').first.text).to eq group_label.name
+ end
+
+ it 'has valid text' do
+ expect(result.text).to eq "See #{group_label.name}"
+ end
+
+ it 'ignores invalid IDs on the referenced label' do
+ exp = act = "See #{invalidate_reference(reference)}"
+
+ expect(reference_filter(act).to_html).to eq exp
end
end
end
diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb
new file mode 100644
index 00000000000..3fe2c7f5d5d
--- /dev/null
+++ b/spec/lib/banzai/filter/math_filter_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+
+describe Banzai::Filter::MathFilter, lib: true do
+ include FilterSpecHelper
+
+ it 'leaves regular inline code unchanged' do
+ input = "<code>2+2</code>"
+ doc = filter(input)
+
+ expect(doc.to_s).to eq input
+ end
+
+ it 'removes surrounding dollar signs and adds class code, math and js-render-math' do
+ doc = filter("$<code>2+2</code>$")
+
+ expect(doc.to_s).to eq '<code class="code math js-render-math" data-math-style="inline">2+2</code>'
+ end
+
+ it 'only removes surrounding dollar signs' do
+ doc = filter("test $<code>2+2</code>$ test")
+ before = doc.xpath('descendant-or-self::text()[1]').first
+ after = doc.xpath('descendant-or-self::text()[3]').first
+
+ expect(before.to_s).to eq 'test '
+ expect(after.to_s).to eq ' test'
+ end
+
+ it 'only removes surrounding single dollar sign' do
+ doc = filter("test $$<code>2+2</code>$$ test")
+ before = doc.xpath('descendant-or-self::text()[1]').first
+ after = doc.xpath('descendant-or-self::text()[3]').first
+
+ expect(before.to_s).to eq 'test $'
+ expect(after.to_s).to eq '$ test'
+ end
+
+ it 'adds data-math-style inline attribute to inline math' do
+ doc = filter('$<code>2+2</code>$')
+ code = doc.xpath('descendant-or-self::code').first
+
+ expect(code['data-math-style']).to eq 'inline'
+ end
+
+ it 'adds class code and math to inline math' do
+ doc = filter('$<code>2+2</code>$')
+ code = doc.xpath('descendant-or-self::code').first
+
+ expect(code[:class]).to include("code")
+ expect(code[:class]).to include("math")
+ end
+
+ it 'adds js-render-math class to inline math' do
+ doc = filter('$<code>2+2</code>$')
+ code = doc.xpath('descendant-or-self::code').first
+
+ expect(code[:class]).to include("js-render-math")
+ end
+
+ # Cases with faulty syntax. Should be a no-op
+
+ it 'ignores cases with missing dolar sign at the end' do
+ input = "test $<code>2+2</code> test"
+ doc = filter(input)
+
+ expect(doc.to_s).to eq input
+ end
+
+ it 'ignores cases with missing dolar sign at the beginning' do
+ input = "test <code>2+2</code>$ test"
+ doc = filter(input)
+
+ expect(doc.to_s).to eq input
+ end
+
+ it 'ignores dollar signs if it is not adjacent' do
+ input = '<p>We check strictly $<code>2+2</code> and <code>2+2</code>$ </p>'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq input
+ end
+
+ # Display math
+
+ it 'adds data-math-style display attribute to display math' do
+ doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>')
+ pre = doc.xpath('descendant-or-self::pre').first
+
+ expect(pre['data-math-style']).to eq 'display'
+ end
+
+ it 'adds js-render-math class to display math' do
+ doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>')
+ pre = doc.xpath('descendant-or-self::pre').first
+
+ expect(pre[:class]).to include("js-render-math")
+ end
+
+ it 'ignores code blocks that are not math' do
+ input = '<pre class="code highlight js-syntax-highlight plaintext" v-pre="true"><code>2+2</code></pre>'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq input
+ end
+
+ it 'requires the pre to contain both code and math' do
+ input = '<pre class="highlight js-syntax-highlight plaintext math" v-pre="true"><code>2+2</code></pre>'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq input
+ end
+
+ it 'dollar signs around to display math' do
+ doc = filter('$<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>$')
+ before = doc.xpath('descendant-or-self::text()[1]').first
+ after = doc.xpath('descendant-or-self::text()[3]').first
+
+ expect(before.to_s).to eq '$'
+ expect(after.to_s).to eq '$'
+ end
+end
diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
index 274258a045c..275010c1a2c 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
include FilterSpecHelper
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:merge) { create(:merge_request, source_project: project) }
it 'requires project context' do
@@ -86,23 +86,97 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
end
end
- context 'cross-project reference' do
- let(:namespace) { create(:namespace, name: 'cross-reference') }
- let(:project2) { create(:project, :public, namespace: namespace) }
- let(:merge) { create(:merge_request, source_project: project2) }
- let(:reference) { merge.to_reference(project) }
+ context 'cross-project / cross-namespace complete reference' do
+ let(:project2) { create(:empty_project, :public) }
+ let(:merge) { create(:merge_request, source_project: project2) }
+ let(:reference) { "#{project2.path_with_namespace}!#{merge.iid}" }
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
expect(doc.css('a').first.attr('href')).
to eq urls.namespace_project_merge_request_url(project2.namespace,
- project, merge)
+ project2, merge)
end
- it 'links with adjacent text' do
+ it 'link has valid text' do
doc = reference_filter("Merge (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+
+ expect(doc.css('a').first.text).to eq(reference)
+ end
+
+ it 'has valid text' do
+ doc = reference_filter("Merge (#{reference}.)")
+
+ expect(doc.text).to eq("Merge (#{reference}.)")
+ end
+
+ it 'ignores invalid merge IDs on the referenced project' do
+ exp = act = "Merge #{invalidate_reference(reference)}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'cross-project / same-namespace complete reference' do
+ let(:namespace) { create(:namespace) }
+ let(:project) { create(:empty_project, :public, namespace: namespace) }
+ let(:project2) { create(:empty_project, :public, namespace: namespace) }
+ let!(:merge) { create(:merge_request, source_project: project2) }
+ let(:reference) { "#{project2.path_with_namespace}!#{merge.iid}" }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_merge_request_url(project2.namespace,
+ project2, merge)
+ end
+
+ it 'link has valid text' do
+ doc = reference_filter("Merge (#{reference}.)")
+
+ expect(doc.css('a').first.text).to eq("#{project2.path}!#{merge.iid}")
+ end
+
+ it 'has valid text' do
+ doc = reference_filter("Merge (#{reference}.)")
+
+ expect(doc.text).to eq("Merge (#{project2.path}!#{merge.iid}.)")
+ end
+
+ it 'ignores invalid merge IDs on the referenced project' do
+ exp = act = "Merge #{invalidate_reference(reference)}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'cross-project shorthand reference' do
+ let(:namespace) { create(:namespace) }
+ let(:project) { create(:empty_project, :public, namespace: namespace) }
+ let(:project2) { create(:empty_project, :public, namespace: namespace) }
+ let!(:merge) { create(:merge_request, source_project: project2) }
+ let(:reference) { "#{project2.path}!#{merge.iid}" }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_merge_request_url(project2.namespace,
+ project2, merge)
+ end
+
+ it 'link has valid text' do
+ doc = reference_filter("Merge (#{reference}.)")
+
+ expect(doc.css('a').first.text).to eq("#{project2.path}!#{merge.iid}")
+ end
+
+ it 'has valid text' do
+ doc = reference_filter("Merge (#{reference}.)")
+
+ expect(doc.text).to eq("Merge (#{project2.path}!#{merge.iid}.)")
end
it 'ignores invalid merge IDs on the referenced project' do
diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
index 7419863d848..73b5edb99b3 100644
--- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
@@ -148,13 +148,51 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
end
end
- describe 'cross project milestone references' do
- let(:another_project) { create(:empty_project, :public) }
- let(:project_path) { another_project.path_with_namespace }
- let(:milestone) { create(:milestone, project: another_project) }
- let(:reference) { milestone.to_reference(project) }
+ describe 'cross-project / cross-namespace complete reference' do
+ let(:namespace) { create(:namespace) }
+ let(:another_project) { create(:empty_project, :public, namespace: namespace) }
+ let(:milestone) { create(:milestone, project: another_project) }
+ let(:reference) { "#{another_project.path_with_namespace}%#{milestone.iid}" }
+ let!(:result) { reference_filter("See #{reference}") }
- let!(:result) { reference_filter("See #{reference}") }
+ it 'points to referenced project milestone page' do
+ expect(result.css('a').first.attr('href')).to eq urls.
+ namespace_project_milestone_url(another_project.namespace,
+ another_project,
+ milestone)
+ end
+
+ it 'link has valid text' do
+ doc = reference_filter("See (#{reference}.)")
+
+ expect(doc.css('a').first.text).
+ to eq("#{milestone.name} in #{another_project.path_with_namespace}")
+ end
+
+ it 'has valid text' do
+ doc = reference_filter("See (#{reference}.)")
+
+ expect(doc.text).
+ to eq("See (#{milestone.name} in #{another_project.path_with_namespace}.)")
+ end
+
+ it 'escapes the name attribute' do
+ allow_any_instance_of(Milestone).to receive(:title).and_return(%{"></a>whatever<a title="})
+
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.text).
+ to eq "#{milestone.name} in #{another_project.path_with_namespace}"
+ end
+ end
+
+ describe 'cross-project / same-namespace complete reference' do
+ let(:namespace) { create(:namespace) }
+ let(:project) { create(:empty_project, :public, namespace: namespace) }
+ let(:another_project) { create(:empty_project, :public, namespace: namespace) }
+ let(:milestone) { create(:milestone, project: another_project) }
+ let(:reference) { "#{another_project.path_with_namespace}%#{milestone.iid}" }
+ let!(:result) { reference_filter("See #{reference}") }
it 'points to referenced project milestone page' do
expect(result.css('a').first.attr('href')).to eq urls.
@@ -163,14 +201,66 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
milestone)
end
- it 'contains cross project content' do
- expect(result.css('a').first.text).to eq "#{milestone.name} in #{project_path}"
+ it 'link has valid text' do
+ doc = reference_filter("See (#{reference}.)")
+
+ expect(doc.css('a').first.text).
+ to eq("#{milestone.name} in #{another_project.path}")
+ end
+
+ it 'has valid text' do
+ doc = reference_filter("See (#{reference}.)")
+
+ expect(doc.text).
+ to eq("See (#{milestone.name} in #{another_project.path}.)")
end
it 'escapes the name attribute' do
allow_any_instance_of(Milestone).to receive(:title).and_return(%{"></a>whatever<a title="})
+
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.text).
+ to eq "#{milestone.name} in #{another_project.path}"
+ end
+ end
+
+ describe 'cross project shorthand reference' do
+ let(:namespace) { create(:namespace) }
+ let(:project) { create(:empty_project, :public, namespace: namespace) }
+ let(:another_project) { create(:empty_project, :public, namespace: namespace) }
+ let(:milestone) { create(:milestone, project: another_project) }
+ let(:reference) { "#{another_project.path}%#{milestone.iid}" }
+ let!(:result) { reference_filter("See #{reference}") }
+
+ it 'points to referenced project milestone page' do
+ expect(result.css('a').first.attr('href')).to eq urls.
+ namespace_project_milestone_url(another_project.namespace,
+ another_project,
+ milestone)
+ end
+
+ it 'link has valid text' do
+ doc = reference_filter("See (#{reference}.)")
+
+ expect(doc.css('a').first.text).
+ to eq("#{milestone.name} in #{another_project.path}")
+ end
+
+ it 'has valid text' do
+ doc = reference_filter("See (#{reference}.)")
+
+ expect(doc.text).
+ to eq("See (#{milestone.name} in #{another_project.path}.)")
+ end
+
+ it 'escapes the name attribute' do
+ allow_any_instance_of(Milestone).to receive(:title).and_return(%{"></a>whatever<a title="})
+
doc = reference_filter("See #{reference}")
- expect(doc.css('a').first.text).to eq "#{milestone.name} in #{project_path}"
+
+ expect(doc.css('a').first.text).
+ to eq "#{milestone.name} in #{another_project.path}"
end
end
end
diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb
index f181125156b..0140a91c7ba 100644
--- a/spec/lib/banzai/filter/redactor_filter_spec.rb
+++ b/spec/lib/banzai/filter/redactor_filter_spec.rb
@@ -28,31 +28,39 @@ describe Banzai::Filter::RedactorFilter, lib: true do
and_return(parser_class)
end
- it 'removes unpermitted Project references' do
- user = create(:user)
- project = create(:empty_project)
+ context 'valid projects' do
+ before { allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(true) }
- link = reference_link(project: project.id, reference_type: 'test')
- doc = filter(link, current_user: user)
+ it 'allows permitted Project references' do
+ user = create(:user)
+ project = create(:empty_project)
+ project.team << [user, :master]
+
+ link = reference_link(project: project.id, reference_type: 'test')
+ doc = filter(link, current_user: user)
- expect(doc.css('a').length).to eq 0
+ expect(doc.css('a').length).to eq 1
+ end
end
- it 'allows permitted Project references' do
- user = create(:user)
- project = create(:empty_project)
- project.team << [user, :master]
+ context 'invalid projects' do
+ before { allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(false) }
- link = reference_link(project: project.id, reference_type: 'test')
- doc = filter(link, current_user: user)
+ it 'removes unpermitted references' do
+ user = create(:user)
+ project = create(:empty_project)
- expect(doc.css('a').length).to eq 1
- end
+ link = reference_link(project: project.id, reference_type: 'test')
+ doc = filter(link, current_user: user)
- it 'handles invalid Project references' do
- link = reference_link(project: 12345, reference_type: 'test')
+ expect(doc.css('a').length).to eq 0
+ end
+
+ it 'handles invalid references' do
+ link = reference_link(project: 12345, reference_type: 'test')
- expect { filter(link) }.not_to raise_error
+ expect { filter(link) }.not_to raise_error
+ end
end
end
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index 2bfa51deb20..df2dd173b57 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -175,7 +175,7 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
allow_any_instance_of(described_class).to receive(:uri_type).and_return(:raw)
doc = filter(image(escaped))
- expect(doc.at_css('img')['src']).to match '/raw/'
+ expect(doc.at_css('img')['src']).to eq "/#{project_path}/raw/#{Addressable::URI.escape(ref)}/#{escaped}"
end
context 'when requested path is a file in the repo' do
diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
index 9b92d1a3926..e036514d283 100644
--- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
@@ -79,11 +79,11 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
end
end
- context 'cross-project reference' do
- let(:namespace) { create(:namespace, name: 'cross-reference') }
+ context 'cross-project / cross-namespace complete reference' do
+ let(:namespace) { create(:namespace) }
let(:project2) { create(:empty_project, :public, namespace: namespace) }
- let(:snippet) { create(:project_snippet, project: project2) }
- let(:reference) { snippet.to_reference(project) }
+ let!(:snippet) { create(:project_snippet, project: project2) }
+ let(:reference) { "#{project2.path_with_namespace}$#{snippet.id}" }
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
@@ -92,9 +92,82 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet)
end
- it 'links with adjacent text' do
+ it 'link has valid text' do
doc = reference_filter("See (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+
+ expect(doc.css('a').first.text).to eql(reference)
+ end
+
+ it 'has valid text' do
+ doc = reference_filter("See (#{reference}.)")
+
+ expect(doc.text).to eql("See (#{reference}.)")
+ end
+
+ it 'ignores invalid snippet IDs on the referenced project' do
+ exp = act = "See #{invalidate_reference(reference)}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'cross-project / same-namespace complete reference' do
+ let(:namespace) { create(:namespace) }
+ let(:project) { create(:empty_project, :public, namespace: namespace) }
+ let(:project2) { create(:empty_project, :public, namespace: namespace) }
+ let!(:snippet) { create(:project_snippet, project: project2) }
+ let(:reference) { "#{project2.path_with_namespace}$#{snippet.id}" }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet)
+ end
+
+ it 'link has valid text' do
+ doc = reference_filter("See (#{project2.path}$#{snippet.id}.)")
+
+ expect(doc.css('a').first.text).to eql("#{project2.path}$#{snippet.id}")
+ end
+
+ it 'has valid text' do
+ doc = reference_filter("See (#{project2.path}$#{snippet.id}.)")
+
+ expect(doc.text).to eql("See (#{project2.path}$#{snippet.id}.)")
+ end
+
+ it 'ignores invalid snippet IDs on the referenced project' do
+ exp = act = "See #{invalidate_reference(reference)}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'cross-project shorthand reference' do
+ let(:namespace) { create(:namespace) }
+ let(:project) { create(:empty_project, :public, namespace: namespace) }
+ let(:project2) { create(:empty_project, :public, namespace: namespace) }
+ let!(:snippet) { create(:project_snippet, project: project2) }
+ let(:reference) { "#{project2.path}$#{snippet.id}" }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet)
+ end
+
+ it 'link has valid text' do
+ doc = reference_filter("See (#{project2.path}$#{snippet.id}.)")
+
+ expect(doc.css('a').first.text).to eql("#{project2.path}$#{snippet.id}")
+ end
+
+ it 'has valid text' do
+ doc = reference_filter("See (#{project2.path}$#{snippet.id}.)")
+
+ expect(doc.text).to eql("See (#{project2.path}$#{snippet.id}.)")
end
it 'ignores invalid snippet IDs on the referenced project' do
diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
index 356dd01a03a..70b31f3a880 100644
--- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
+++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
@@ -22,7 +22,7 @@ describe Banzai::Filter::TableOfContentsFilter, lib: true do
html = header(i, "Header #{i}")
doc = filter(html)
- expect(doc.css("h#{i} a").first.attr('id')).to eq "header-#{i}"
+ expect(doc.css("h#{i} a").first.attr('id')).to eq "user-content-header-#{i}"
end
end
@@ -32,7 +32,12 @@ describe Banzai::Filter::TableOfContentsFilter, lib: true do
expect(doc.css('h1 a').first.attr('class')).to eq 'anchor'
end
- it 'links to the id' do
+ it 'has a namespaced id' do
+ doc = filter(header(1, 'Header'))
+ expect(doc.css('h1 a').first.attr('id')).to eq 'user-content-header'
+ end
+
+ it 'links to the non-namespaced id' do
doc = filter(header(1, 'Header'))
expect(doc.css('h1 a').first.attr('href')).to eq '#header'
end
@@ -40,29 +45,29 @@ describe Banzai::Filter::TableOfContentsFilter, lib: true do
describe 'generated IDs' do
it 'translates spaces to dashes' do
doc = filter(header(1, 'This header has spaces in it'))
- expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-has-spaces-in-it'
+ expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-has-spaces-in-it'
end
it 'squeezes multiple spaces and dashes' do
doc = filter(header(1, 'This---header is poorly-formatted'))
- expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-poorly-formatted'
+ expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-is-poorly-formatted'
end
it 'removes punctuation' do
doc = filter(header(1, "This, header! is, filled. with @ punctuation?"))
- expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-filled-with-punctuation'
+ expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-is-filled-with-punctuation'
end
it 'appends a unique number to duplicates' do
doc = filter(header(1, 'One') + header(2, 'One'))
- expect(doc.css('h1 a').first.attr('id')).to eq 'one'
- expect(doc.css('h2 a').first.attr('id')).to eq 'one-1'
+ expect(doc.css('h1 a').first.attr('href')).to eq '#one'
+ expect(doc.css('h2 a').first.attr('href')).to eq '#one-1'
end
it 'supports Unicode' do
doc = filter(header(1, '한글'))
- expect(doc.css('h1 a').first.attr('id')).to eq '한글'
+ expect(doc.css('h1 a').first.attr('id')).to eq 'user-content-한글'
expect(doc.css('h1 a').first.attr('href')).to eq '#한글'
end
end
diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb
index 9095d2b1345..aa127f0179d 100644
--- a/spec/lib/banzai/reference_parser/base_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb
@@ -27,41 +27,12 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do
let(:link) { empty_html_link }
context 'when the link has a data-project attribute' do
- it 'returns the nodes if the attribute value equals the current project ID' do
+ it 'checks if user can read the resource' do
link['data-project'] = project.id.to_s
- expect(Ability).not_to receive(:allowed?)
- expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
- end
-
- it 'returns the nodes if the user can read the project' do
- other_project = create(:empty_project, :public)
-
- link['data-project'] = other_project.id.to_s
-
- expect(Ability).to receive(:allowed?).
- with(user, :read_project, other_project).
- and_return(true)
-
- expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
- end
-
- it 'returns an empty Array when the attribute value is empty' do
- link['data-project'] = ''
-
- expect(subject.nodes_visible_to_user(user, [link])).to eq([])
- end
-
- it 'returns an empty Array when the user can not read the project' do
- other_project = create(:empty_project, :public)
-
- link['data-project'] = other_project.id.to_s
-
- expect(Ability).to receive(:allowed?).
- with(user, :read_project, other_project).
- and_return(false)
+ expect(subject).to receive(:can_read_reference?).with(user, project)
- expect(subject.nodes_visible_to_user(user, [link])).to eq([])
+ subject.nodes_visible_to_user(user, [link])
end
end
diff --git a/spec/lib/banzai/reference_parser/commit_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
index 0b76d29fce0..412ffa77c36 100644
--- a/spec/lib/banzai/reference_parser/commit_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
@@ -8,6 +8,14 @@ describe Banzai::ReferenceParser::CommitParser, lib: true do
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
+ describe '#nodes_visible_to_user' do
+ context 'when the link has a data-issue attribute' do
+ before { link['data-commit'] = 123 }
+
+ it_behaves_like "referenced feature visibility", "repository"
+ end
+ end
+
describe '#referenced_by' do
context 'when the link has a data-project attribute' do
before do
diff --git a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
index ba982f38542..96e55b0997a 100644
--- a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
@@ -8,6 +8,14 @@ describe Banzai::ReferenceParser::CommitRangeParser, lib: true do
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
+ describe '#nodes_visible_to_user' do
+ context 'when the link has a data-issue attribute' do
+ before { link['data-commit-range'] = '123..456' }
+
+ it_behaves_like "referenced feature visibility", "repository"
+ end
+ end
+
describe '#referenced_by' do
context 'when the link has a data-project attribute' do
before do
diff --git a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
index a6ef8394fe7..50a5d1a19ba 100644
--- a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
@@ -8,6 +8,14 @@ describe Banzai::ReferenceParser::ExternalIssueParser, lib: true do
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
+ describe '#nodes_visible_to_user' do
+ context 'when the link has a data-issue attribute' do
+ before { link['data-external-issue'] = 123 }
+
+ it_behaves_like "referenced feature visibility", "issues"
+ end
+ end
+
describe '#referenced_by' do
context 'when the link has a data-project attribute' do
before do
diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
index 85cfe728b6a..6873b7b85f9 100644
--- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
@@ -4,10 +4,10 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do
include ReferenceParserHelpers
let(:project) { create(:empty_project, :public) }
- let(:user) { create(:user) }
- let(:issue) { create(:issue, project: project) }
- subject { described_class.new(project, user) }
- let(:link) { empty_html_link }
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project) }
+ let(:link) { empty_html_link }
+ subject { described_class.new(project, user) }
describe '#nodes_visible_to_user' do
context 'when the link has a data-issue attribute' do
@@ -15,6 +15,8 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do
link['data-issue'] = issue.id.to_s
end
+ it_behaves_like "referenced feature visibility", "issues"
+
it 'returns the nodes when the user can read the issue' do
expect(Ability).to receive(:issues_readable_by_user).
with([issue], user).
diff --git a/spec/lib/banzai/reference_parser/label_parser_spec.rb b/spec/lib/banzai/reference_parser/label_parser_spec.rb
index 77fda47f0e7..8c540d35ddd 100644
--- a/spec/lib/banzai/reference_parser/label_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/label_parser_spec.rb
@@ -9,6 +9,14 @@ describe Banzai::ReferenceParser::LabelParser, lib: true do
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
+ describe '#nodes_visible_to_user' do
+ context 'when the link has a data-issue attribute' do
+ before { link['data-label'] = label.id.to_s }
+
+ it_behaves_like "referenced feature visibility", "issues", "merge_requests"
+ end
+ end
+
describe '#referenced_by' do
describe 'when the link has a data-label attribute' do
context 'using an existing label ID' do
diff --git a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
index cf89ad598ea..cb69ca16800 100644
--- a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
@@ -8,6 +8,19 @@ describe Banzai::ReferenceParser::MergeRequestParser, lib: true do
subject { described_class.new(merge_request.target_project, user) }
let(:link) { empty_html_link }
+ describe '#nodes_visible_to_user' do
+ context 'when the link has a data-issue attribute' do
+ let(:project) { merge_request.target_project }
+
+ before do
+ project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+ link['data-merge-request'] = merge_request.id.to_s
+ end
+
+ it_behaves_like "referenced feature visibility", "merge_requests"
+ end
+ end
+
describe '#referenced_by' do
describe 'when the link has a data-merge-request attribute' do
context 'using an existing merge request ID' do
diff --git a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
index 6aa45a22cc4..2d4d589ae34 100644
--- a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
@@ -9,6 +9,14 @@ describe Banzai::ReferenceParser::MilestoneParser, lib: true do
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
+ describe '#nodes_visible_to_user' do
+ context 'when the link has a data-issue attribute' do
+ before { link['data-milestone'] = milestone.id.to_s }
+
+ it_behaves_like "referenced feature visibility", "issues", "merge_requests"
+ end
+ end
+
describe '#referenced_by' do
describe 'when the link has a data-milestone attribute' do
context 'using an existing milestone ID' do
diff --git a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
index 59127b7c5d1..d217a775802 100644
--- a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
@@ -9,6 +9,14 @@ describe Banzai::ReferenceParser::SnippetParser, lib: true do
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
+ describe '#nodes_visible_to_user' do
+ context 'when the link has a data-issue attribute' do
+ before { link['data-snippet'] = snippet.id.to_s }
+
+ it_behaves_like "referenced feature visibility", "snippets"
+ end
+ end
+
describe '#referenced_by' do
describe 'when the link has a data-snippet attribute' do
context 'using an existing snippet ID' do
diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb
index 4e7f82a6e09..fafc2cec546 100644
--- a/spec/lib/banzai/reference_parser/user_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb
@@ -103,6 +103,8 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
it 'returns the nodes if the attribute value equals the current project ID' do
link['data-project'] = project.id.to_s
+ # Ensure that we dont call for Ability.allowed?
+ # When project_id in the node is equal to current project ID
expect(Ability).not_to receive(:allowed?)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
diff --git a/spec/lib/bitbucket/collection_spec.rb b/spec/lib/bitbucket/collection_spec.rb
new file mode 100644
index 00000000000..015a7f80e03
--- /dev/null
+++ b/spec/lib/bitbucket/collection_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+# Emulates paginator. It returns 2 pages with results
+class TestPaginator
+ def initialize
+ @current_page = 0
+ end
+
+ def items
+ @current_page += 1
+
+ raise StopIteration if @current_page > 2
+
+ ["result_1_page_#{@current_page}", "result_2_page_#{@current_page}"]
+ end
+end
+
+describe Bitbucket::Collection do
+ it "iterates paginator" do
+ collection = described_class.new(TestPaginator.new)
+
+ expect(collection.to_a).to match(["result_1_page_1", "result_2_page_1", "result_1_page_2", "result_2_page_2"])
+ end
+end
diff --git a/spec/lib/bitbucket/connection_spec.rb b/spec/lib/bitbucket/connection_spec.rb
new file mode 100644
index 00000000000..14faeb231a9
--- /dev/null
+++ b/spec/lib/bitbucket/connection_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Bitbucket::Connection do
+ before do
+ allow_any_instance_of(described_class).to receive(:provider).and_return(double(app_id: '', app_secret: ''))
+ end
+
+ describe '#get' do
+ it 'calls OAuth2::AccessToken::get' do
+ expect_any_instance_of(OAuth2::AccessToken).to receive(:get).and_return(double(parsed: true))
+
+ connection = described_class.new({})
+
+ connection.get('/users')
+ end
+ end
+
+ describe '#expired?' do
+ it 'calls connection.expired?' do
+ expect_any_instance_of(OAuth2::AccessToken).to receive(:expired?).and_return(true)
+
+ expect(described_class.new({}).expired?).to be_truthy
+ end
+ end
+
+ describe '#refresh!' do
+ it 'calls connection.refresh!' do
+ response = double(token: nil, expires_at: nil, expires_in: nil, refresh_token: nil)
+
+ expect_any_instance_of(OAuth2::AccessToken).to receive(:refresh!).and_return(response)
+
+ described_class.new({}).refresh!
+ end
+ end
+end
diff --git a/spec/lib/bitbucket/page_spec.rb b/spec/lib/bitbucket/page_spec.rb
new file mode 100644
index 00000000000..04d5a0470b1
--- /dev/null
+++ b/spec/lib/bitbucket/page_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe Bitbucket::Page do
+ let(:response) { { 'values' => [{ 'username' => 'Ben' }], 'pagelen' => 2, 'next' => '' } }
+
+ before do
+ # Autoloading hack
+ Bitbucket::Representation::User.new({})
+ end
+
+ describe '#items' do
+ it 'returns collection of needed objects' do
+ page = described_class.new(response, :user)
+
+ expect(page.items.first).to be_a(Bitbucket::Representation::User)
+ expect(page.items.count).to eq(1)
+ end
+ end
+
+ describe '#attrs' do
+ it 'returns attributes' do
+ page = described_class.new(response, :user)
+
+ expect(page.attrs.keys).to include(:pagelen, :next)
+ end
+ end
+
+ describe '#next?' do
+ it 'returns true' do
+ page = described_class.new(response, :user)
+
+ expect(page.next?).to be_truthy
+ end
+
+ it 'returns false' do
+ response['next'] = nil
+ page = described_class.new(response, :user)
+
+ expect(page.next?).to be_falsey
+ end
+ end
+
+ describe '#next' do
+ it 'returns next attribute' do
+ page = described_class.new(response, :user)
+
+ expect(page.next).to eq('')
+ end
+ end
+end
diff --git a/spec/lib/bitbucket/paginator_spec.rb b/spec/lib/bitbucket/paginator_spec.rb
new file mode 100644
index 00000000000..2c972da682e
--- /dev/null
+++ b/spec/lib/bitbucket/paginator_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Bitbucket::Paginator do
+ let(:last_page) { double(:page, next?: false, items: ['item_2']) }
+ let(:first_page) { double(:page, next?: true, next: last_page, items: ['item_1']) }
+
+ describe 'items' do
+ it 'return items and raises StopIteration in the end' do
+ paginator = described_class.new(nil, nil, nil)
+
+ allow(paginator).to receive(:fetch_next_page).and_return(first_page)
+ expect(paginator.items).to match(['item_1'])
+
+ allow(paginator).to receive(:fetch_next_page).and_return(last_page)
+ expect(paginator.items).to match(['item_2'])
+
+ allow(paginator).to receive(:fetch_next_page).and_return(nil)
+ expect{ paginator.items }.to raise_error(StopIteration)
+ end
+ end
+end
diff --git a/spec/lib/bitbucket/representation/comment_spec.rb b/spec/lib/bitbucket/representation/comment_spec.rb
new file mode 100644
index 00000000000..fec243a9f96
--- /dev/null
+++ b/spec/lib/bitbucket/representation/comment_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Bitbucket::Representation::Comment do
+ describe '#author' do
+ it { expect(described_class.new('user' => { 'username' => 'Ben' }).author).to eq('Ben') }
+ it { expect(described_class.new({}).author).to be_nil }
+ end
+
+ describe '#note' do
+ it { expect(described_class.new('content' => { 'raw' => 'Text' }).note).to eq('Text') }
+ it { expect(described_class.new({}).note).to be_nil }
+ end
+
+ describe '#created_at' do
+ it { expect(described_class.new('created_on' => Date.today).created_at).to eq(Date.today) }
+ end
+
+ describe '#updated_at' do
+ it { expect(described_class.new('updated_on' => Date.today).updated_at).to eq(Date.today) }
+ it { expect(described_class.new('created_on' => Date.today).updated_at).to eq(Date.today) }
+ end
+end
diff --git a/spec/lib/bitbucket/representation/issue_spec.rb b/spec/lib/bitbucket/representation/issue_spec.rb
new file mode 100644
index 00000000000..20f47224aa8
--- /dev/null
+++ b/spec/lib/bitbucket/representation/issue_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Bitbucket::Representation::Issue do
+ describe '#iid' do
+ it { expect(described_class.new('id' => 1).iid).to eq(1) }
+ end
+
+ describe '#kind' do
+ it { expect(described_class.new('kind' => 'bug').kind).to eq('bug') }
+ end
+
+ describe '#milestone' do
+ it { expect(described_class.new({ 'milestone' => { 'name' => '1.0' } }).milestone).to eq('1.0') }
+ it { expect(described_class.new({}).milestone).to be_nil }
+ end
+
+ describe '#author' do
+ it { expect(described_class.new({ 'reporter' => { 'username' => 'Ben' } }).author).to eq('Ben') }
+ it { expect(described_class.new({}).author).to be_nil }
+ end
+
+ describe '#description' do
+ it { expect(described_class.new({ 'content' => { 'raw' => 'Text' } }).description).to eq('Text') }
+ it { expect(described_class.new({}).description).to be_nil }
+ end
+
+ describe '#state' do
+ it { expect(described_class.new({ 'state' => 'invalid' }).state).to eq('closed') }
+ it { expect(described_class.new({ 'state' => 'wontfix' }).state).to eq('closed') }
+ it { expect(described_class.new({ 'state' => 'resolved' }).state).to eq('closed') }
+ it { expect(described_class.new({ 'state' => 'duplicate' }).state).to eq('closed') }
+ it { expect(described_class.new({ 'state' => 'closed' }).state).to eq('closed') }
+ it { expect(described_class.new({ 'state' => 'opened' }).state).to eq('opened') }
+ end
+
+ describe '#title' do
+ it { expect(described_class.new('title' => 'Issue').title).to eq('Issue') }
+ end
+
+ describe '#created_at' do
+ it { expect(described_class.new('created_on' => Date.today).created_at).to eq(Date.today) }
+ end
+
+ describe '#updated_at' do
+ it { expect(described_class.new('edited_on' => Date.today).updated_at).to eq(Date.today) }
+ end
+end
diff --git a/spec/lib/bitbucket/representation/pull_request_comment_spec.rb b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb
new file mode 100644
index 00000000000..673dcf22ce8
--- /dev/null
+++ b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Bitbucket::Representation::PullRequestComment do
+ describe '#iid' do
+ it { expect(described_class.new('id' => 1).iid).to eq(1) }
+ end
+
+ describe '#file_path' do
+ it { expect(described_class.new('inline' => { 'path' => '/path' }).file_path).to eq('/path') }
+ end
+
+ describe '#old_pos' do
+ it { expect(described_class.new('inline' => { 'from' => 3 }).old_pos).to eq(3) }
+ end
+
+ describe '#new_pos' do
+ it { expect(described_class.new('inline' => { 'to' => 3 }).new_pos).to eq(3) }
+ end
+
+ describe '#parent_id' do
+ it { expect(described_class.new({ 'parent' => { 'id' => 2 } }).parent_id).to eq(2) }
+ it { expect(described_class.new({}).parent_id).to be_nil }
+ end
+
+ describe '#inline?' do
+ it { expect(described_class.new('inline' => {}).inline?).to be_truthy }
+ it { expect(described_class.new({}).inline?).to be_falsey }
+ end
+
+ describe '#has_parent?' do
+ it { expect(described_class.new('parent' => {}).has_parent?).to be_truthy }
+ it { expect(described_class.new({}).has_parent?).to be_falsey }
+ end
+end
diff --git a/spec/lib/bitbucket/representation/pull_request_spec.rb b/spec/lib/bitbucket/representation/pull_request_spec.rb
new file mode 100644
index 00000000000..30453528be4
--- /dev/null
+++ b/spec/lib/bitbucket/representation/pull_request_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Bitbucket::Representation::PullRequest do
+ describe '#iid' do
+ it { expect(described_class.new('id' => 1).iid).to eq(1) }
+ end
+
+ describe '#author' do
+ it { expect(described_class.new({ 'author' => { 'username' => 'Ben' } }).author).to eq('Ben') }
+ it { expect(described_class.new({}).author).to be_nil }
+ end
+
+ describe '#description' do
+ it { expect(described_class.new({ 'description' => 'Text' }).description).to eq('Text') }
+ it { expect(described_class.new({}).description).to be_nil }
+ end
+
+ describe '#state' do
+ it { expect(described_class.new({ 'state' => 'MERGED' }).state).to eq('merged') }
+ it { expect(described_class.new({ 'state' => 'DECLINED' }).state).to eq('closed') }
+ it { expect(described_class.new({}).state).to eq('opened') }
+ end
+
+ describe '#title' do
+ it { expect(described_class.new('title' => 'Issue').title).to eq('Issue') }
+ end
+
+ describe '#source_branch_name' do
+ it { expect(described_class.new({ source: { branch: { name: 'feature' } } }.with_indifferent_access).source_branch_name).to eq('feature') }
+ it { expect(described_class.new({ source: {} }.with_indifferent_access).source_branch_name).to be_nil }
+ end
+
+ describe '#source_branch_sha' do
+ it { expect(described_class.new({ source: { commit: { hash: 'abcd123' } } }.with_indifferent_access).source_branch_sha).to eq('abcd123') }
+ it { expect(described_class.new({ source: {} }.with_indifferent_access).source_branch_sha).to be_nil }
+ end
+
+ describe '#target_branch_name' do
+ it { expect(described_class.new({ destination: { branch: { name: 'master' } } }.with_indifferent_access).target_branch_name).to eq('master') }
+ it { expect(described_class.new({ destination: {} }.with_indifferent_access).target_branch_name).to be_nil }
+ end
+
+ describe '#target_branch_sha' do
+ it { expect(described_class.new({ destination: { commit: { hash: 'abcd123' } } }.with_indifferent_access).target_branch_sha).to eq('abcd123') }
+ it { expect(described_class.new({ destination: {} }.with_indifferent_access).target_branch_sha).to be_nil }
+ end
+end
diff --git a/spec/lib/bitbucket/representation/user_spec.rb b/spec/lib/bitbucket/representation/user_spec.rb
new file mode 100644
index 00000000000..f79ff4edb7b
--- /dev/null
+++ b/spec/lib/bitbucket/representation/user_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe Bitbucket::Representation::User do
+ describe '#username' do
+ it 'returns correct value' do
+ user = described_class.new('username' => 'Ben')
+
+ expect(user.username).to eq('Ben')
+ end
+ 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 84f21631719..ff5dcc06ab3 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -1124,8 +1124,8 @@ EOT
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra config should be a hash")
end
- it "returns errors if there are unknown parameters that are hashes, but doesn't have a script" do
- config = YAML.dump({ extra: { services: "test" } })
+ it "returns errors if services configuration is not correct" do
+ config = YAML.dump({ extra: { script: 'rspec', services: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra:services config should be an array of strings")
diff --git a/spec/lib/constraints/group_url_constrainer_spec.rb b/spec/lib/constraints/group_url_constrainer_spec.rb
index f0b75a664f2..96dacdc5cd2 100644
--- a/spec/lib/constraints/group_url_constrainer_spec.rb
+++ b/spec/lib/constraints/group_url_constrainer_spec.rb
@@ -1,10 +1,30 @@
require 'spec_helper'
describe GroupUrlConstrainer, lib: true do
- let!(:username) { create(:group, path: 'gitlab-org') }
+ let!(:group) { create(:group, path: 'gitlab') }
- describe '#find_resource' do
- it { expect(!!subject.find_resource('gitlab-org')).to be_truthy }
- it { expect(!!subject.find_resource('gitlab-com')).to be_falsey }
+ describe '#matches?' do
+ context 'valid request' do
+ let(:request) { build_request(group.path) }
+
+ it { expect(subject.matches?(request)).to be_truthy }
+ end
+
+ context 'valid request for nested group' do
+ let!(:nested_group) { create(:group, path: 'nested', parent: group) }
+ let!(:request) { build_request('gitlab/nested') }
+
+ it { expect(subject.matches?(request)).to be_truthy }
+ end
+
+ context 'invalid request' do
+ let(:request) { build_request('foo') }
+
+ it { expect(subject.matches?(request)).to be_falsey }
+ end
+ end
+
+ def build_request(path)
+ double(:request, params: { id: path })
end
end
diff --git a/spec/lib/constraints/namespace_url_constrainer_spec.rb b/spec/lib/constraints/namespace_url_constrainer_spec.rb
deleted file mode 100644
index 7814711fe27..00000000000
--- a/spec/lib/constraints/namespace_url_constrainer_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'spec_helper'
-
-describe NamespaceUrlConstrainer, lib: true do
- let!(:group) { create(:group, path: 'gitlab') }
-
- describe '#matches?' do
- context 'existing namespace' do
- it { expect(subject.matches?(request '/gitlab')).to be_truthy }
- it { expect(subject.matches?(request '/gitlab.atom')).to be_truthy }
- it { expect(subject.matches?(request '/gitlab/')).to be_truthy }
- it { expect(subject.matches?(request '//gitlab/')).to be_truthy }
- end
-
- context 'non-existing namespace' do
- it { expect(subject.matches?(request '/gitlab-ce')).to be_falsey }
- it { expect(subject.matches?(request '/gitlab.ce')).to be_falsey }
- it { expect(subject.matches?(request '/g/gitlab')).to be_falsey }
- it { expect(subject.matches?(request '/.gitlab')).to be_falsey }
- end
-
- context 'relative url' do
- before do
- allow(Gitlab::Application.config).to receive(:relative_url_root) { '/gitlab' }
- end
-
- it { expect(subject.matches?(request '/gitlab/gitlab')).to be_truthy }
- it { expect(subject.matches?(request '/gitlab/gitlab-ce')).to be_falsey }
- it { expect(subject.matches?(request '/gitlab/')).to be_falsey }
- end
- end
-
- def request(path)
- OpenStruct.new(path: path)
- end
-end
diff --git a/spec/lib/constraints/project_url_constrainer_spec.rb b/spec/lib/constraints/project_url_constrainer_spec.rb
new file mode 100644
index 00000000000..94266f6653b
--- /dev/null
+++ b/spec/lib/constraints/project_url_constrainer_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe ProjectUrlConstrainer, lib: true do
+ let!(:project) { create(:project) }
+ let!(:namespace) { project.namespace }
+
+ describe '#matches?' do
+ context 'valid request' do
+ let(:request) { build_request(namespace.path, project.path) }
+
+ it { expect(subject.matches?(request)).to be_truthy }
+ end
+
+ context 'invalid request' do
+ context "non-existing project" do
+ let(:request) { build_request('foo', 'bar') }
+
+ it { expect(subject.matches?(request)).to be_falsey }
+ end
+
+ context "project id ending with .git" do
+ let(:request) { build_request(namespace.path, project.path + '.git') }
+
+ it { expect(subject.matches?(request)).to be_falsey }
+ end
+ end
+ end
+
+ def build_request(namespace, project)
+ double(:request, params: { namespace_id: namespace, id: project })
+ end
+end
diff --git a/spec/lib/constraints/user_url_constrainer_spec.rb b/spec/lib/constraints/user_url_constrainer_spec.rb
index 4b26692672f..207b6fe6c9e 100644
--- a/spec/lib/constraints/user_url_constrainer_spec.rb
+++ b/spec/lib/constraints/user_url_constrainer_spec.rb
@@ -1,10 +1,23 @@
require 'spec_helper'
describe UserUrlConstrainer, lib: true do
- let!(:username) { create(:user, username: 'dz') }
+ let!(:user) { create(:user, username: 'dz') }
- describe '#find_resource' do
- it { expect(!!subject.find_resource('dz')).to be_truthy }
- it { expect(!!subject.find_resource('john')).to be_falsey }
+ describe '#matches?' do
+ context 'valid request' do
+ let(:request) { build_request(user.username) }
+
+ it { expect(subject.matches?(request)).to be_truthy }
+ end
+
+ context 'invalid request' do
+ let(:request) { build_request('foo') }
+
+ it { expect(subject.matches?(request)).to be_falsey }
+ end
+ end
+
+ def build_request(username)
+ double(:request, params: { username: username })
end
end
diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb
index a6d8e6927e0..ec2f66b1136 100644
--- a/spec/lib/event_filter_spec.rb
+++ b/spec/lib/event_filter_spec.rb
@@ -7,6 +7,10 @@ describe EventFilter, lib: true do
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) }
@@ -21,6 +25,11 @@ describe EventFilter, lib: true do
expect(events).to contain_exactly(merged_event)
end
+ it 'applies issue filter' do
+ events = EventFilter.new(EventFilter.issue).apply_filter(Event.all)
+ expect(events).to contain_exactly(created_event, updated_event, closed_event, reopened_event)
+ end
+
it 'applies comments filter' do
events = EventFilter.new(EventFilter.comments).apply_filter(Event.all)
expect(events).to contain_exactly(comments_event)
@@ -33,17 +42,17 @@ describe EventFilter, lib: true do
it 'applies all filter' do
events = EventFilter.new(EventFilter.all).apply_filter(Event.all)
- expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event)
+ expect(events).to contain_exactly(push_event, merged_event, created_event, updated_event, closed_event, reopened_event, comments_event, joined_event, left_event)
end
it 'applies no filter' do
events = EventFilter.new(nil).apply_filter(Event.all)
- expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event)
+ expect(events).to contain_exactly(push_event, merged_event, created_event, updated_event, closed_event, reopened_event, comments_event, joined_event, left_event)
end
it 'applies unknown filter' do
events = EventFilter.new('').apply_filter(Event.all)
- expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event)
+ expect(events).to contain_exactly(push_event, merged_event, created_event, updated_event, closed_event, reopened_event, comments_event, joined_event, left_event)
end
end
end
diff --git a/spec/lib/gitlab/allowable_spec.rb b/spec/lib/gitlab/allowable_spec.rb
new file mode 100644
index 00000000000..87733d53e92
--- /dev/null
+++ b/spec/lib/gitlab/allowable_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Gitlab::Allowable do
+ subject do
+ Class.new.include(described_class).new
+ end
+
+ describe '#can?' do
+ let(:user) { create(:user) }
+
+ context 'when user is allowed to do something' do
+ let(:project) { create(:empty_project, :public) }
+
+ it 'reports correct ability to perform action' do
+ expect(subject.can?(user, :read_project, project)).to be true
+ end
+ end
+
+ context 'when user is not allowed to do something' do
+ let(:project) { create(:empty_project, :private) }
+
+ it 'reports correct ability to perform action' do
+ expect(subject.can?(user, :read_project, project)).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 4aba783dc33..f3843ca64ff 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -11,7 +11,7 @@ module Gitlab
it "converts the input using Asciidoctor and default options" do
expected_asciidoc_opts = {
safe: :secure,
- backend: :html5,
+ backend: :gitlab_html5,
attributes: described_class::DEFAULT_ADOC_ATTRS
}
@@ -27,7 +27,7 @@ module Gitlab
it "merges the options with default ones" do
expected_asciidoc_opts = {
safe: :safe,
- backend: :html5,
+ backend: :gitlab_html5,
attributes: described_class::DEFAULT_ADOC_ATTRS + ['foo']
}
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index c9d64e99f88..f251c0dd25a 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -47,54 +47,76 @@ describe Gitlab::Auth, lib: true do
project.create_drone_ci_service(active: true)
project.drone_ci_service.update(token: 'token')
- ip = 'ip'
-
- expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'drone-ci-token')
- expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities))
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'drone-ci-token')
+ expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities))
end
it 'recognizes master passwords' do
user = create(:user, password: 'password')
- ip = 'ip'
- expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
- 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))
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username)
+ 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)
- ip = 'ip'
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))
+ 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)
- ip = 'ip'
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))
+ 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
- it 'recognizes OAuth tokens' 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)
- ip = 'ip'
+ context "while using OAuth tokens as passwords" do
+ it 'succeeds for OAuth tokens with the `api` scope' 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")
+
+ 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))
+ end
- 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))
+ 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")
+
+ 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
+ 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'])
+
+ 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))
+ 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
end
it 'returns double nil for invalid credentials' do
login = 'foo'
- ip = 'ip'
- expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: login)
- expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new)
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login)
+ expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new)
end
end
diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/gitlab/backup/manager_spec.rb
new file mode 100644
index 00000000000..1b749d1bd39
--- /dev/null
+++ b/spec/lib/gitlab/backup/manager_spec.rb
@@ -0,0 +1,127 @@
+require 'spec_helper'
+
+describe Backup::Manager, lib: true do
+ describe '#remove_old' do
+ let(:progress) { StringIO.new }
+
+ let(:files) do
+ [
+ '1451606400_2016_01_01_gitlab_backup.tar',
+ '1451520000_2015_12_31_gitlab_backup.tar',
+ '1450742400_2015_12_22_gitlab_backup.tar',
+ '1449878400_gitlab_backup.tar',
+ '1449014400_gitlab_backup.tar',
+ 'manual_gitlab_backup.tar'
+ ]
+ end
+
+ before do
+ allow(Dir).to receive(:chdir).and_yield
+ allow(Dir).to receive(:glob).and_return(files)
+ allow(FileUtils).to receive(:rm)
+ allow(Time).to receive(:now).and_return(Time.utc(2016))
+
+ allow(progress).to receive(:puts)
+ allow(progress).to receive(:print)
+
+ allow_any_instance_of(String).to receive(:color) do |string, _color|
+ string
+ end
+
+ @old_progress = $progress # rubocop:disable Style/GlobalVars
+ $progress = progress # rubocop:disable Style/GlobalVars
+ end
+
+ after do
+ $progress = @old_progress # rubocop:disable Style/GlobalVars
+ end
+
+ context 'when keep_time is zero' do
+ before do
+ allow(Gitlab.config.backup).to receive(:keep_time).and_return(0)
+
+ subject.remove_old
+ end
+
+ it 'removes no files' do
+ expect(FileUtils).not_to have_received(:rm)
+ end
+
+ it 'prints a skipped message' do
+ expect(progress).to have_received(:puts).with('skipping')
+ end
+ end
+
+ context 'when there are no files older than keep_time' do
+ before do
+ allow(Gitlab.config.backup).to receive(:keep_time).and_return(2592000)
+
+ subject.remove_old
+ end
+
+ it 'removes no files' do
+ expect(FileUtils).not_to have_received(:rm)
+ end
+
+ it 'prints a done message' do
+ expect(progress).to have_received(:puts).with('done. (0 removed)')
+ end
+ end
+
+ context 'when keep_time is set to remove files' do
+ before do
+ allow(Gitlab.config.backup).to receive(:keep_time).and_return(1)
+
+ subject.remove_old
+ end
+
+ it 'removes matching files with a human-readable timestamp' do
+ expect(FileUtils).to have_received(:rm).with(files[1])
+ expect(FileUtils).to have_received(:rm).with(files[2])
+ end
+
+ it 'removes matching files without a human-readable timestamp' do
+ expect(FileUtils).to have_received(:rm).with(files[3])
+ expect(FileUtils).to have_received(:rm).with(files[4])
+ end
+
+ it 'does not remove files that are not old enough' do
+ expect(FileUtils).not_to have_received(:rm).with(files[0])
+ end
+
+ it 'does not remove non-matching files' do
+ expect(FileUtils).not_to have_received(:rm).with(files[5])
+ end
+
+ it 'prints a done message' do
+ expect(progress).to have_received(:puts).with('done. (4 removed)')
+ end
+ end
+
+ context 'when removing a file fails' do
+ let(:file) { files[1] }
+ let(:message) { "Permission denied @ unlink_internal - #{file}" }
+
+ before do
+ allow(Gitlab.config.backup).to receive(:keep_time).and_return(1)
+ allow(FileUtils).to receive(:rm).with(file).and_raise(Errno::EACCES, message)
+
+ subject.remove_old
+ end
+
+ it 'removes the remaining expected files' do
+ expect(FileUtils).to have_received(:rm).with(files[2])
+ expect(FileUtils).to have_received(:rm).with(files[3])
+ expect(FileUtils).to have_received(:rm).with(files[4])
+ end
+
+ it 'sets the correct removed count' do
+ expect(progress).to have_received(:puts).with('done. (3 removed)')
+ end
+
+ it 'prints the error from file that could not be removed' do
+ expect(progress).to have_received(:puts).with(a_string_matching(message))
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/badge/build/status_spec.rb b/spec/lib/gitlab/badge/build/status_spec.rb
index 38eebb2a176..70f03021d36 100644
--- a/spec/lib/gitlab/badge/build/status_spec.rb
+++ b/spec/lib/gitlab/badge/build/status_spec.rb
@@ -69,8 +69,8 @@ describe Gitlab::Badge::Build::Status do
new_build.success!
end
- it 'reports the compound status' do
- expect(badge.status).to eq 'failed'
+ it 'does not take outdated pipeline into account' do
+ expect(badge.status).to eq 'success'
end
end
end
diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb
deleted file mode 100644
index 7543c29bcc4..00000000000
--- a/spec/lib/gitlab/bitbucket_import/client_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::BitbucketImport::Client, lib: true do
- include ImportSpecHelper
-
- let(:token) { '123456' }
- let(:secret) { 'secret' }
- let(:client) { Gitlab::BitbucketImport::Client.new(token, secret) }
-
- before do
- stub_omniauth_provider('bitbucket')
- end
-
- it 'all OAuth client options are symbols' do
- client.consumer.options.keys.each do |key|
- expect(key).to be_kind_of(Symbol)
- end
- end
-
- context 'issues' do
- let(:per_page) { 50 }
- let(:count) { 95 }
- let(:sample_issues) do
- issues = []
-
- count.times do |i|
- issues << { local_id: i }
- end
-
- issues
- end
- let(:first_sample_data) { { count: count, issues: sample_issues[0..per_page - 1] } }
- let(:second_sample_data) { { count: count, issues: sample_issues[per_page..count] } }
- let(:project_id) { 'namespace/repo' }
-
- it 'retrieves issues over a number of pages' do
- stub_request(:get,
- "https://bitbucket.org/api/1.0/repositories/#{project_id}/issues?limit=50&sort=utc_created_on&start=0").
- to_return(status: 200,
- body: first_sample_data.to_json,
- headers: {})
-
- stub_request(:get,
- "https://bitbucket.org/api/1.0/repositories/#{project_id}/issues?limit=50&sort=utc_created_on&start=50").
- to_return(status: 200,
- body: second_sample_data.to_json,
- headers: {})
-
- issues = client.issues(project_id)
- expect(issues.count).to eq(95)
- end
- end
-
- context 'project import' do
- it 'calls .from_project with no errors' do
- project = create(:empty_project)
- project.import_url = "ssh://git@bitbucket.org/test/test.git"
- project.create_or_update_import_data(credentials:
- { user: "git",
- password: nil,
- bb_session: { bitbucket_access_token: "test",
- bitbucket_access_token_secret: "test" } })
-
- expect { described_class.from_project(project) }.not_to raise_error
- end
- end
-end
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index aa00f32becb..53f3c73ade4 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -18,15 +18,21 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
"closed" # undocumented status
]
end
+
let(:sample_issues_statuses) do
issues = []
statuses.map.with_index do |status, index|
issues << {
- local_id: index,
- status: status,
+ id: index,
+ state: status,
title: "Issue #{index}",
- content: "Some content to issue #{index}"
+ kind: 'bug',
+ content: {
+ raw: "Some content to issue #{index}",
+ markup: "markdown",
+ html: "Some content to issue #{index}"
+ }
}
end
@@ -34,14 +40,16 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
end
let(:project_identifier) { 'namespace/repo' }
+
let(:data) do
{
'bb_session' => {
- 'bitbucket_access_token' => "123456",
- 'bitbucket_access_token_secret' => "secret"
+ 'bitbucket_token' => "123456",
+ 'bitbucket_refresh_token' => "secret"
}
}
end
+
let(:project) do
create(
:project,
@@ -49,11 +57,13 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
import_data: ProjectImportData.new(credentials: data)
)
end
+
let(:importer) { Gitlab::BitbucketImport::Importer.new(project) }
+
let(:issues_statuses_sample_data) do
{
count: sample_issues_statuses.count,
- issues: sample_issues_statuses
+ values: sample_issues_statuses
}
end
@@ -61,26 +71,46 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
before do
stub_request(
:get,
- "https://bitbucket.org/api/1.0/repositories/#{project_identifier}"
- ).to_return(status: 200, body: { has_issues: true }.to_json)
+ "https://api.bitbucket.org/2.0/repositories/#{project_identifier}"
+ ).to_return(status: 200,
+ headers: { "Content-Type" => "application/json" },
+ body: { has_issues: true, full_name: project_identifier }.to_json)
stub_request(
:get,
- "https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues?limit=50&sort=utc_created_on&start=0"
- ).to_return(status: 200, body: issues_statuses_sample_data.to_json)
+ "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues?pagelen=50&sort=created_on"
+ ).to_return(status: 200,
+ headers: { "Content-Type" => "application/json" },
+ body: issues_statuses_sample_data.to_json)
+
+ stub_request(:get, "https://api.bitbucket.org/2.0/repositories/namespace/repo?pagelen=50&sort=created_on").
+ with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer', 'User-Agent' => 'Faraday v0.9.2' }).
+ to_return(status: 200,
+ body: "",
+ headers: {})
sample_issues_statuses.each_with_index do |issue, index|
stub_request(
:get,
- "https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues/#{issue[:local_id]}/comments"
+ "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues/#{issue[:id]}/comments?pagelen=50&sort=created_on"
).to_return(
status: 200,
- body: [{ author_info: { username: "username" }, utc_created_on: index }].to_json
+ headers: { "Content-Type" => "application/json" },
+ body: { author_info: { username: "username" }, utc_created_on: index }.to_json
)
end
+
+ stub_request(
+ :get,
+ "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/pullrequests?pagelen=50&sort=created_on&state=ALL"
+ ).to_return(status: 200,
+ headers: { "Content-Type" => "application/json" },
+ body: {}.to_json)
end
it 'map statuses to open or closed' do
+ # HACK: Bitbucket::Representation.const_get('Issue') seems to return ::Issue without this
+ Bitbucket::Representation::Issue.new({})
importer.execute
expect(project.issues.where(state: "closed").size).to eq(5)
diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
index e1c60e07b4d..b6d052a4612 100644
--- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
@@ -2,14 +2,18 @@ require 'spec_helper'
describe Gitlab::BitbucketImport::ProjectCreator, lib: true do
let(:user) { create(:user) }
+
let(:repo) do
- {
- name: 'Vim',
- slug: 'vim',
- is_private: true,
- owner: "asd"
- }.with_indifferent_access
+ double(name: 'Vim',
+ slug: 'vim',
+ description: 'Test repo',
+ is_private: true,
+ owner: "asd",
+ full_name: 'Vim repo',
+ visibility_level: Gitlab::VisibilityLevel::PRIVATE,
+ clone_url: 'ssh://git@bitbucket.org/asd/vim.git')
end
+
let(:namespace){ create(:group, owner: user) }
let(:token) { "asdasd12345" }
let(:secret) { "sekrettt" }
@@ -22,7 +26,7 @@ describe Gitlab::BitbucketImport::ProjectCreator, lib: true do
it 'creates project' do
allow_any_instance_of(Project).to receive(:add_import_job)
- project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, user, access_params)
+ project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, 'vim', namespace, user, access_params)
project = project_creator.execute
expect(project.import_url).to eq("ssh://git@bitbucket.org/asd/vim.git")
diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb
new file mode 100644
index 00000000000..bfc6818ac08
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/command_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Command, service: true do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ describe '#execute' do
+ subject { described_class.new(project, user, params).execute }
+
+ context 'when no command is available' do
+ let(:params) { { text: 'issue show 1' } }
+ let(:project) { create(:project, has_external_issue_tracker: true) }
+
+ it 'displays 404 messages' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to start_with('404 not found')
+ end
+ end
+
+ context 'when an unknown command is triggered' do
+ let(:params) { { command: '/gitlab', text: "unknown command 123" } }
+
+ it 'displays the help message' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to start_with('Available commands')
+ expect(subject[:text]).to match('/gitlab issue show')
+ end
+ end
+
+ context 'the user can not create an issue' do
+ let(:params) { { text: "issue create my new issue" } }
+
+ it 'rejects the actions' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to start_with('Whoops! That action is not allowed')
+ end
+ end
+
+ context 'issue is successfully created' do
+ let(:params) { { text: "issue create my new issue" } }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ it 'presents the issue' do
+ expect(subject[:text]).to match("my new issue")
+ end
+
+ it 'shows a link to the new issue' do
+ expect(subject[:text]).to match(/\/issues\/\d+/)
+ end
+ end
+
+ context 'when trying to do deployment' do
+ let(:params) { { text: 'deploy staging to production' } }
+ let!(:build) { create(:ci_build, project: project) }
+ let!(:staging) { create(:environment, name: 'staging', project: project) }
+ let!(:deployment) { create(:deployment, environment: staging, deployable: build) }
+ let!(:manual) do
+ create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'first', environment: 'production')
+ end
+
+ 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')
+ end
+ end
+
+ context 'and user does have deployment permission' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'returns action' do
+ expect(subject[:text]).to include('Deployment from staging to production started')
+ expect(subject[:response_type]).to be(:in_channel)
+ end
+
+ context 'when duplicate action exists' do
+ let!(:manual2) do
+ create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'second', environment: 'production')
+ end
+
+ it 'returns error' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to include('Too many actions defined')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat_commands/deploy_spec.rb b/spec/lib/gitlab/chat_commands/deploy_spec.rb
new file mode 100644
index 00000000000..bd8099c92da
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/deploy_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Deploy, service: true do
+ describe '#execute' do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:regex_match) { described_class.match('deploy staging to production') }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ subject do
+ described_class.new(project, user).execute(regex_match)
+ end
+
+ context 'if no environment is defined' do
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'with environment' do
+ let!(:staging) { create(:environment, name: 'staging', project: project) }
+ let!(:build) { create(:ci_build, project: project) }
+ let!(:deployment) { create(:deployment, environment: staging, deployable: build) }
+
+ context 'without actions' do
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'with action' do
+ let!(:manual1) do
+ create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'first', 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')
+ end
+
+ context 'when duplicate action exists' do
+ let!(:manual2) do
+ create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'second', environment: 'production')
+ end
+
+ it 'returns error' do
+ expect(subject.type).to eq(:error)
+ expect(subject.message).to include('Too many actions defined')
+ end
+ end
+
+ context 'when teardown action exists' do
+ let!(:teardown) do
+ create(:ci_build, :manual, :teardown_environment,
+ project: project, pipeline: build.pipeline,
+ 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')
+ end
+ end
+ end
+ end
+ end
+
+ describe 'self.match' do
+ it 'matches the environment' do
+ match = described_class.match('deploy staging to production')
+
+ expect(match[:from]).to eq('staging')
+ expect(match[:to]).to eq('production')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_create_spec.rb
new file mode 100644
index 00000000000..6c71e79ff6d
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/issue_create_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::IssueCreate, service: true do
+ describe '#execute' do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:regex_match) { described_class.match("issue create bird is the word") }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ subject do
+ described_class.new(project, user).execute(regex_match)
+ end
+
+ context 'without description' do
+ it 'creates the issue' do
+ expect { subject }.to change { project.issues.count }.by(1)
+
+ expect(subject.title).to eq('bird is the word')
+ end
+ end
+
+ context 'with description' do
+ let(:description) { "Surfin bird" }
+ let(:regex_match) { described_class.match("issue create bird is the word\n#{description}") }
+
+ it 'creates the issue with description' do
+ subject
+
+ expect(Issue.last.description).to eq(description)
+ end
+ end
+
+ context "with more newlines between the title and the description" do
+ let(:description) { "Surfin bird" }
+ let(:regex_match) { described_class.match("issue create bird is the word\n\n#{description}\n") }
+
+ it 'creates the issue' do
+ expect { subject }.to change { project.issues.count }.by(1)
+ end
+ end
+ end
+
+ describe '.match' do
+ it 'matches the title without description' do
+ match = described_class.match("issue create my title")
+
+ expect(match[:title]).to eq('my title')
+ expect(match[:description]).to eq("")
+ end
+
+ it 'matches the title with description' do
+ match = described_class.match("issue create my title\n\ndescription")
+
+ expect(match[:title]).to eq('my title')
+ expect(match[:description]).to eq('description')
+ end
+
+ it 'matches the alias new' do
+ match = described_class.match("issue new my title")
+
+ expect(match).not_to be_nil
+ expect(match[:title]).to eq('my title')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat_commands/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/issue_search_spec.rb
new file mode 100644
index 00000000000..24c06a967fa
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/issue_search_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::IssueSearch, service: true do
+ describe '#execute' do
+ let!(:issue) { create(:issue, title: 'find me') }
+ let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') }
+ let(:project) { issue.project }
+ let(:user) { issue.author }
+ let(:regex_match) { described_class.match("issue search find") }
+
+ subject do
+ described_class.new(project, user).execute(regex_match)
+ end
+
+ context 'when the user has no access' do
+ it 'only returns the open issues' do
+ expect(subject).not_to include(confidential)
+ end
+ end
+
+ context 'the user has access' do
+ before do
+ project.team << [user, :master]
+ end
+
+ it 'returns all results' do
+ expect(subject).to include(confidential, issue)
+ end
+ end
+
+ context 'without hits on the query' do
+ it 'returns an empty collection' do
+ expect(subject).to be_empty
+ end
+ end
+ end
+
+ describe 'self.match' do
+ let(:query) { "my search keywords" }
+ it 'matches the query' do
+ match = described_class.match("issue search #{query}")
+
+ expect(match[:query]).to eq(query)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat_commands/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/issue_show_spec.rb
new file mode 100644
index 00000000000..2eab73e49e5
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/issue_show_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::IssueShow, service: true do
+ describe '#execute' do
+ let(:issue) { create(:issue) }
+ let(:project) { issue.project }
+ let(:user) { issue.author }
+ let(:regex_match) { described_class.match("issue show #{issue.iid}") }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ subject do
+ described_class.new(project, user).execute(regex_match)
+ end
+
+ context 'the issue exists' do
+ it 'returns the issue' do
+ expect(subject.iid).to be issue.iid
+ end
+
+ 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
+ end
+ end
+ end
+
+ context 'the issue does not exist' do
+ let(:regex_match) { described_class.match("issue show 2343242") }
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+ end
+
+ describe 'self.match' do
+ it 'matches the iid' do
+ match = described_class.match("issue show 123")
+
+ expect(match[:iid]).to eq("123")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat_name_token_spec.rb b/spec/lib/gitlab/chat_name_token_spec.rb
new file mode 100644
index 00000000000..8c1e6efa9db
--- /dev/null
+++ b/spec/lib/gitlab/chat_name_token_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::ChatNameToken, lib: true do
+ context 'when using unknown token' do
+ let(:token) { }
+
+ subject { described_class.new(token).get }
+
+ it 'returns empty data' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when storing data' do
+ let(:data) { { key: 'value' } }
+
+ subject { described_class.new(@token) }
+
+ before do
+ @token = described_class.new.store!(data)
+ end
+
+ it 'returns stored data' do
+ expect(subject.get).to eq(data)
+ end
+
+ context 'and after deleting them' do
+ before do
+ subject.delete
+ end
+
+ it 'data are removed' do
+ expect(subject.get).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/credentials/factory_spec.rb b/spec/lib/gitlab/ci/build/credentials/factory_spec.rb
new file mode 100644
index 00000000000..10b4b7a8826
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/credentials/factory_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Credentials::Factory do
+ let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) }
+
+ subject { Gitlab::Ci::Build::Credentials::Factory.new(build).create! }
+
+ class TestProvider
+ def initialize(build); end
+ end
+
+ before do
+ allow_any_instance_of(Gitlab::Ci::Build::Credentials::Factory).to receive(:providers).and_return([TestProvider])
+ end
+
+ context 'when provider is valid' do
+ before do
+ allow_any_instance_of(TestProvider).to receive(:valid?).and_return(true)
+ end
+
+ it 'generates an array of credentials objects' do
+ is_expected.to be_kind_of(Array)
+ is_expected.not_to be_empty
+ expect(subject.first).to be_kind_of(TestProvider)
+ end
+ end
+
+ context 'when provider is not valid' do
+ before do
+ allow_any_instance_of(TestProvider).to receive(:valid?).and_return(false)
+ end
+
+ it 'generates an array without specific credential object' do
+ is_expected.to be_kind_of(Array)
+ is_expected.to be_empty
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/credentials/registry_spec.rb b/spec/lib/gitlab/ci/build/credentials/registry_spec.rb
new file mode 100644
index 00000000000..84e44dd53e2
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/credentials/registry_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Credentials::Registry do
+ let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let(:registry_url) { 'registry.example.com:5005' }
+
+ subject { Gitlab::Ci::Build::Credentials::Registry.new(build) }
+
+ before do
+ stub_container_registry_config(host_port: registry_url)
+ end
+
+ it 'contains valid DockerRegistry credentials' do
+ expect(subject).to be_kind_of(Gitlab::Ci::Build::Credentials::Registry)
+
+ expect(subject.username).to eq 'gitlab-ci-token'
+ expect(subject.password).to eq build.token
+ expect(subject.url).to eq registry_url
+ expect(subject.type).to eq 'registry'
+ end
+
+ describe '.valid?' do
+ subject { Gitlab::Ci::Build::Credentials::Registry.new(build).valid? }
+
+ context 'when registry is enabled' do
+ before do
+ stub_container_registry_config(enabled: true)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when registry is disabled' do
+ before do
+ stub_container_registry_config(enabled: false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb
new file mode 100644
index 00000000000..5c31423fdee
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Artifacts do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validation' do
+ context 'when entry config value is correct' do
+ let(:config) { { paths: %w[public/] } }
+
+ describe '#value' do
+ it 'returns artifacs configuration' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ describe '#errors' do
+ context 'when value of attribute is invalid' do
+ let(:config) { { name: 10 } }
+
+ it 'reports error' do
+ expect(entry.errors)
+ .to include 'artifacts name should be a string'
+ end
+ end
+
+ context 'when there is an unknown key present' do
+ let(:config) { { test: 100 } }
+
+ it 'reports error' do
+ expect(entry.errors)
+ .to include 'artifacts config contains unknown keys: test'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/attributable_spec.rb b/spec/lib/gitlab/ci/config/entry/attributable_spec.rb
new file mode 100644
index 00000000000..fde03c51e2c
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/attributable_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Attributable do
+ let(:node) { Class.new }
+ let(:instance) { node.new }
+
+ before do
+ node.include(described_class)
+
+ node.class_eval do
+ attributes :name, :test
+ end
+ end
+
+ context 'config is a hash' do
+ before do
+ allow(instance)
+ .to receive(:config)
+ .and_return({ name: 'some name', test: 'some test' })
+ end
+
+ it 'returns the value of config' do
+ expect(instance.name).to eq 'some name'
+ expect(instance.test).to eq 'some test'
+ end
+
+ it 'returns no method error for unknown attributes' do
+ expect { instance.unknown }.to raise_error(NoMethodError)
+ end
+ end
+
+ context 'config is not a hash' do
+ before do
+ allow(instance)
+ .to receive(:config)
+ .and_return('some test')
+ end
+
+ it 'returns nil' do
+ expect(instance.test).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/boolean_spec.rb b/spec/lib/gitlab/ci/config/entry/boolean_spec.rb
new file mode 100644
index 00000000000..5f067cad93c
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/boolean_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Boolean do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when entry config value is valid' do
+ let(:config) { false }
+
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to eq false
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not valid' do
+ let(:config) { ['incorrect'] }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'boolean config should be a boolean value'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
new file mode 100644
index 00000000000..70a327c5183
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Cache do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ before { entry.compose! }
+
+ context 'when entry config value is correct' do
+ let(:config) do
+ { key: 'some key',
+ untracked: true,
+ paths: ['some/path/'] }
+ end
+
+ describe '#value' do
+ it 'returns hash value' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ describe '#errors' do
+ context 'when is not a hash' do
+ let(:config) { 'ls' }
+
+ it 'reports errors with config value' do
+ expect(entry.errors)
+ .to include 'cache config should be a hash'
+ end
+ end
+
+ context 'when descendants are invalid' do
+ let(:config) { { key: 1 } }
+
+ it 'reports error with descendants' do
+ expect(entry.errors)
+ .to include 'key config should be a string or symbol'
+ end
+ end
+
+ context 'when there is an unknown key present' do
+ let(:config) { { invalid: true } }
+
+ it 'reports error with descendants' do
+ expect(entry.errors)
+ .to include 'cache config contains unknown keys: invalid'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/commands_spec.rb b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
new file mode 100644
index 00000000000..b8b0825a1c7
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Commands do
+ let(:entry) { described_class.new(config) }
+
+ context 'when entry config value is an array' do
+ let(:config) { ['ls', 'pwd'] }
+
+ describe '#value' do
+ it 'returns array of strings' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#errors' do
+ it 'does not append errors' do
+ expect(entry.errors).to be_empty
+ end
+ end
+ end
+
+ context 'when entry config value is a string' do
+ let(:config) { 'ls' }
+
+ describe '#value' do
+ it 'returns array with single element' do
+ expect(entry.value).to eq ['ls']
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not valid' do
+ let(:config) { 1 }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'commands config should be a ' \
+ 'string or an array of strings'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/configurable_spec.rb b/spec/lib/gitlab/ci/config/entry/configurable_spec.rb
new file mode 100644
index 00000000000..ae7e628b5b5
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/configurable_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Configurable do
+ let(:entry) { Class.new }
+
+ before do
+ entry.include(described_class)
+ end
+
+ describe 'validations' do
+ let(:validator) { entry.validator.new(instance) }
+
+ before do
+ entry.class_eval do
+ attr_reader :config
+
+ def initialize(config)
+ @config = config
+ end
+ end
+
+ validator.validate
+ end
+
+ context 'when entry validator is invalid' do
+ let(:instance) { entry.new('ls') }
+
+ it 'returns invalid validator' do
+ expect(validator).to be_invalid
+ end
+ end
+
+ context 'when entry instance is valid' do
+ let(:instance) { entry.new(key: 'value') }
+
+ it 'returns valid validator' do
+ expect(validator).to be_valid
+ end
+ end
+ end
+
+ describe 'configured entries' do
+ before do
+ entry.class_eval do
+ entry :object, Object, description: 'test object'
+ end
+ end
+
+ describe '.nodes' do
+ it 'has valid nodes' do
+ expect(entry.nodes).to include :object
+ end
+
+ it 'creates a node factory' do
+ expect(entry.nodes[:object])
+ .to be_an_instance_of Gitlab::Ci::Config::Entry::Factory
+ end
+
+ it 'returns a duplicated factory object' do
+ first_factory = entry.nodes[:object]
+ second_factory = entry.nodes[:object]
+
+ expect(first_factory).not_to be_equal(second_factory)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
new file mode 100644
index 00000000000..d97806295fb
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
@@ -0,0 +1,217 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Environment do
+ let(:entry) { described_class.new(config) }
+
+ before { entry.compose! }
+
+ context 'when configuration is a string' do
+ let(:config) { 'production' }
+
+ describe '#string?' do
+ it 'is string configuration' do
+ expect(entry).to be_string
+ end
+ end
+
+ describe '#hash?' do
+ it 'is not hash configuration' do
+ expect(entry).not_to be_hash
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#value' do
+ it 'returns valid hash' do
+ expect(entry.value).to include(name: 'production')
+ end
+ end
+
+ describe '#name' do
+ it 'returns environment name' do
+ expect(entry.name).to eq 'production'
+ end
+ end
+
+ describe '#url' do
+ it 'returns environment url' do
+ expect(entry.url).to be_nil
+ end
+ end
+ end
+
+ context 'when configuration is a hash' do
+ let(:config) do
+ { name: 'development', url: 'https://example.gitlab.com' }
+ end
+
+ describe '#string?' do
+ it 'is not string configuration' do
+ expect(entry).not_to be_string
+ end
+ end
+
+ describe '#hash?' do
+ it 'is hash configuration' do
+ expect(entry).to be_hash
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#value' do
+ it 'returns valid hash' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#name' do
+ it 'returns environment name' do
+ expect(entry.name).to eq 'development'
+ end
+ end
+
+ describe '#url' do
+ it 'returns environment url' do
+ expect(entry.url).to eq 'https://example.gitlab.com'
+ end
+ end
+ end
+
+ context 'when valid action is used' do
+ let(:config) do
+ { name: 'production',
+ action: 'start' }
+ end
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'when invalid action is used' do
+ let(:config) do
+ { name: 'production',
+ action: 'invalid' }
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'contains error about invalid action' do
+ expect(entry.errors)
+ .to include 'environment action should be start or stop'
+ end
+ end
+ end
+
+ context 'when on_stop is used' do
+ let(:config) do
+ { name: 'production',
+ on_stop: 'close_app' }
+ end
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'when invalid on_stop is used' do
+ let(:config) do
+ { name: 'production',
+ on_stop: false }
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'contains error about invalid action' do
+ expect(entry.errors)
+ .to include 'environment on stop should be a string'
+ end
+ end
+ end
+
+ context 'when variables are used for environment' do
+ let(:config) do
+ { name: 'review/$CI_BUILD_REF_NAME',
+ url: 'https://$CI_BUILD_REF_NAME.review.gitlab.com' }
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when configuration is invalid' do
+ context 'when configuration is an array' do
+ let(:config) { ['env'] }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'contains error about invalid type' do
+ expect(entry.errors)
+ .to include 'environment config should be a hash or a string'
+ end
+ end
+ end
+
+ context 'when environment name is not present' do
+ let(:config) { { url: 'https://example.gitlab.com' } }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors?' do
+ it 'contains error about missing environment name' do
+ expect(entry.errors)
+ .to include "environment name can't be blank"
+ end
+ end
+ end
+
+ context 'when invalid URL is used' do
+ let(:config) { { name: 'test', url: 'invalid-example.gitlab.com' } }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors?' do
+ it 'contains error about invalid URL' do
+ expect(entry.errors)
+ .to include "environment url must be a valid url"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/factory_spec.rb b/spec/lib/gitlab/ci/config/entry/factory_spec.rb
new file mode 100644
index 00000000000..00dad5d9591
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/factory_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Factory do
+ describe '#create!' do
+ let(:factory) { described_class.new(entry) }
+ let(:entry) { Gitlab::Ci::Config::Entry::Script }
+
+ context 'when setting a concrete value' do
+ it 'creates entry with valid value' do
+ entry = factory
+ .value(['ls', 'pwd'])
+ .create!
+
+ expect(entry.value).to eq ['ls', 'pwd']
+ end
+
+ context 'when setting description' do
+ it 'creates entry with description' do
+ entry = factory
+ .value(['ls', 'pwd'])
+ .with(description: 'test description')
+ .create!
+
+ expect(entry.value).to eq ['ls', 'pwd']
+ expect(entry.description).to eq 'test description'
+ end
+ end
+
+ context 'when setting key' do
+ it 'creates entry with custom key' do
+ entry = factory
+ .value(['ls', 'pwd'])
+ .with(key: 'test key')
+ .create!
+
+ expect(entry.key).to eq 'test key'
+ end
+ end
+
+ context 'when setting a parent' do
+ let(:object) { Object.new }
+
+ it 'creates entry with valid parent' do
+ entry = factory
+ .value('ls')
+ .with(parent: object)
+ .create!
+
+ expect(entry.parent).to eq object
+ end
+ end
+ end
+
+ context 'when not setting a value' do
+ it 'raises error' do
+ expect { factory.create! }.to raise_error(
+ Gitlab::Ci::Config::Entry::Factory::InvalidFactory
+ )
+ end
+ end
+
+ context 'when creating entry with nil value' do
+ it 'creates an undefined entry' do
+ entry = factory
+ .value(nil)
+ .create!
+
+ expect(entry)
+ .to be_an_instance_of Gitlab::Ci::Config::Entry::Unspecified
+ end
+ end
+
+ context 'when passing metadata' do
+ let(:entry) { spy('entry') }
+
+ it 'passes metadata as a parameter' do
+ factory
+ .value('some value')
+ .metadata(some: 'hash')
+ .create!
+
+ expect(entry).to have_received(:new)
+ .with('some value', { some: 'hash' })
+ 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
new file mode 100644
index 00000000000..e64c8d46bd8
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb
@@ -0,0 +1,314 @@
+require 'spec_helper'
+
+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
+ end
+
+ it 'returns a hash' do
+ expect(described_class.nodes).to be_a Hash
+ end
+ end
+
+ context 'when configuration is valid' do
+ context 'when some entries defined' do
+ let(:hash) do
+ { before_script: ['ls', 'pwd'],
+ image: 'ruby:2.2',
+ services: ['postgres:9.1', 'mysql:5.5'],
+ variables: { VAR: 'value' },
+ after_script: ['make clean'],
+ stages: ['build', 'pages'],
+ cache: { key: 'k', untracked: true, paths: ['public/'] },
+ rspec: { script: %w[rspec ls] },
+ spinach: { before_script: [], variables: {}, script: 'spinach' } }
+ end
+
+ describe '#compose!' do
+ before { global.compose! }
+
+ it 'creates nodes hash' do
+ expect(global.descendants).to be_an Array
+ end
+
+ it 'creates node object for each entry' do
+ expect(global.descendants.count).to eq 8
+ end
+
+ it 'creates node object using valid class' do
+ expect(global.descendants.first)
+ .to be_an_instance_of Gitlab::Ci::Config::Entry::Script
+ expect(global.descendants.second)
+ .to be_an_instance_of Gitlab::Ci::Config::Entry::Image
+ end
+
+ it 'sets correct description for nodes' do
+ expect(global.descendants.first.description)
+ .to eq 'Script that will be executed before each job.'
+ expect(global.descendants.second.description)
+ .to eq 'Docker image that will be used to execute jobs.'
+ end
+
+ describe '#leaf?' do
+ it 'is not leaf' do
+ expect(global).not_to be_leaf
+ end
+ end
+ end
+
+ context 'when not composed' do
+ describe '#before_script_value' do
+ it 'returns nil' do
+ expect(global.before_script_value).to be nil
+ end
+ end
+
+ describe '#leaf?' do
+ it 'is leaf' do
+ expect(global).to be_leaf
+ end
+ end
+ end
+
+ context 'when composed' do
+ before { global.compose! }
+
+ describe '#errors' do
+ it 'has no errors' do
+ expect(global.errors).to be_empty
+ end
+ end
+
+ describe '#before_script_value' do
+ it 'returns correct script' do
+ expect(global.before_script_value).to eq ['ls', 'pwd']
+ end
+ end
+
+ describe '#image_value' do
+ it 'returns valid image' do
+ expect(global.image_value).to eq 'ruby:2.2'
+ end
+ end
+
+ describe '#services_value' do
+ it 'returns array of services' do
+ expect(global.services_value).to eq ['postgres:9.1', 'mysql:5.5']
+ end
+ end
+
+ describe '#after_script_value' do
+ it 'returns after script' do
+ expect(global.after_script_value).to eq ['make clean']
+ end
+ end
+
+ describe '#variables_value' do
+ it 'returns variables' do
+ expect(global.variables_value).to eq(VAR: 'value')
+ end
+ end
+
+ describe '#stages_value' do
+ context 'when stages key defined' do
+ it 'returns array of stages' do
+ expect(global.stages_value).to eq %w[build pages]
+ end
+ end
+
+ context 'when deprecated types key defined' do
+ let(:hash) do
+ { types: ['test', 'deploy'],
+ rspec: { script: 'rspec' } }
+ end
+
+ it 'returns array of types as stages' do
+ expect(global.stages_value).to eq %w[test deploy]
+ end
+ end
+ end
+
+ describe '#cache_value' do
+ it 'returns cache configuration' do
+ expect(global.cache_value)
+ .to eq(key: 'k', untracked: true, paths: ['public/'])
+ end
+ end
+
+ describe '#jobs_value' do
+ it 'returns jobs configuration' do
+ expect(global.jobs_value).to eq(
+ rspec: { name: :rspec,
+ script: %w[rspec ls],
+ before_script: ['ls', 'pwd'],
+ commands: "ls\npwd\nrspec\nls",
+ image: 'ruby:2.2',
+ services: ['postgres:9.1', 'mysql:5.5'],
+ stage: 'test',
+ cache: { key: 'k', untracked: true, paths: ['public/'] },
+ variables: { VAR: 'value' },
+ after_script: ['make clean'] },
+ spinach: { name: :spinach,
+ before_script: [],
+ script: %w[spinach],
+ commands: 'spinach',
+ image: 'ruby:2.2',
+ services: ['postgres:9.1', 'mysql:5.5'],
+ stage: 'test',
+ cache: { key: 'k', untracked: true, paths: ['public/'] },
+ variables: {},
+ after_script: ['make clean'] },
+ )
+ end
+ end
+ end
+ end
+
+ context 'when most of entires not defined' do
+ before { global.compose! }
+
+ let(:hash) do
+ { cache: { key: 'a' }, rspec: { script: %w[ls] } }
+ end
+
+ describe '#nodes' do
+ it 'instantizes all nodes' do
+ expect(global.descendants.count).to eq 8
+ end
+
+ it 'contains unspecified nodes' do
+ expect(global.descendants.first)
+ .to be_an_instance_of Gitlab::Ci::Config::Entry::Unspecified
+ end
+ end
+
+ describe '#variables_value' do
+ it 'returns default value for variables' do
+ expect(global.variables_value).to eq({})
+ end
+ end
+
+ describe '#stages_value' do
+ it 'returns an array of default stages' do
+ expect(global.stages_value).to eq %w[build test deploy]
+ end
+ end
+
+ describe '#cache_value' do
+ it 'returns correct cache definition' do
+ expect(global.cache_value).to eq(key: 'a')
+ end
+ end
+ end
+
+ ##
+ # When nodes are specified but not defined, we assume that
+ # configuration is valid, and we asume that entry is simply undefined,
+ # despite the fact, that key is present. See issue #18775 for more
+ # details.
+ #
+ context 'when entires specified but not defined' do
+ before { global.compose! }
+
+ let(:hash) do
+ { variables: nil, rspec: { script: 'rspec' } }
+ end
+
+ describe '#variables_value' do
+ it 'undefined entry returns a default value' do
+ expect(global.variables_value).to eq({})
+ end
+ end
+ end
+ end
+
+ context 'when configuration is not valid' do
+ before { global.compose! }
+
+ context 'when before script is not an array' do
+ let(:hash) do
+ { before_script: 'ls' }
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(global).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'reports errors from child nodes' do
+ expect(global.errors)
+ .to include 'before_script config should be an array of strings'
+ end
+ end
+
+ describe '#before_script_value' do
+ it 'returns nil' do
+ expect(global.before_script_value).to be_nil
+ end
+ end
+ end
+
+ context 'when job does not have commands' do
+ let(:hash) do
+ { before_script: ['echo 123'], rspec: { stage: 'test' } }
+ end
+
+ describe '#errors' do
+ it 'reports errors about missing script' do
+ expect(global.errors)
+ .to include "jobs:rspec script can't be blank"
+ end
+ end
+ end
+ end
+
+ context 'when value is not a hash' do
+ let(:hash) { [] }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(global).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'returns error about invalid type' do
+ expect(global.errors.first).to match /should be a hash/
+ end
+ end
+ end
+
+ describe '#specified?' do
+ it 'is concrete entry that is defined' do
+ expect(global.specified?).to be true
+ end
+ end
+
+ describe '#[]' do
+ before { global.compose! }
+
+ let(:hash) do
+ { cache: { key: 'a' }, rspec: { script: 'ls' } }
+ end
+
+ context 'when entry exists' do
+ it 'returns correct entry' do
+ expect(global[:cache])
+ .to be_an_instance_of Gitlab::Ci::Config::Entry::Cache
+ expect(global[:jobs][:rspec][:script].value).to eq ['ls']
+ end
+ end
+
+ context 'when entry does not exist' do
+ it 'always return unspecified node' do
+ expect(global[:some][:unknown][:node])
+ .not_to be_specified
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/hidden_spec.rb b/spec/lib/gitlab/ci/config/entry/hidden_spec.rb
new file mode 100644
index 00000000000..459362761e6
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/hidden_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Hidden do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when entry config value is correct' do
+ let(:config) { [:some, :array] }
+
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to eq [:some, :array]
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ context 'when config is empty' do
+ let(:config) { {} }
+
+ describe '#valid' do
+ it 'is invalid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+ end
+ end
+
+ describe '#leaf?' do
+ it 'is a leaf' do
+ expect(entry).to be_leaf
+ end
+ end
+
+ describe '#relevant?' do
+ it 'is not a relevant entry' do
+ expect(entry).not_to be_relevant
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb
new file mode 100644
index 00000000000..3c99cb0a1ee
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Image do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validation' do
+ context 'when entry config value is correct' do
+ let(:config) { 'ruby:2.2' }
+
+ describe '#value' do
+ it 'returns image string' do
+ expect(entry.value).to eq 'ruby:2.2'
+ end
+ end
+
+ describe '#errors' do
+ it 'does not append errors' do
+ expect(entry.errors).to be_empty
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ let(:config) { ['ruby:2.2'] }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'image config should be a string'
+ end
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ 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
new file mode 100644
index 00000000000..fc9b8b86dc4
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -0,0 +1,148 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Job do
+ let(:entry) { described_class.new(config, name: :rspec) }
+
+ describe 'validations' do
+ before { entry.compose! }
+
+ context 'when entry config value is correct' do
+ let(:config) { { script: 'rspec' } }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'when job name is empty' do
+ let(:entry) { described_class.new(config, name: ''.to_sym) }
+
+ it 'reports error' do
+ expect(entry.errors).to include "job name can't be blank"
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ context 'incorrect config value type' do
+ let(:config) { ['incorrect'] }
+
+ describe '#errors' do
+ it 'reports error about a config type' do
+ expect(entry.errors)
+ .to include 'job config should be a hash'
+ end
+ end
+ end
+
+ context 'when config is empty' do
+ let(:config) { {} }
+
+ describe '#valid' do
+ it 'is invalid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+
+ context 'when unknown keys detected' do
+ let(:config) { { unknown: true } }
+
+ describe '#valid' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+
+ context 'when script is not provided' do
+ let(:config) { { stage: 'test' } }
+
+ it 'returns error about missing script entry' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include "job script can't be blank"
+ end
+ end
+ end
+ end
+
+ describe '#relevant?' do
+ it 'is a relevant entry' do
+ expect(entry).to be_relevant
+ end
+ end
+
+ describe '#compose!' do
+ let(:unspecified) { double('unspecified', 'specified?' => false) }
+
+ let(:specified) do
+ double('specified', 'specified?' => true, value: 'specified')
+ end
+
+ let(:deps) { double('deps', '[]' => unspecified) }
+
+ context 'when job config overrides global config' do
+ before { entry.compose!(deps) }
+
+ let(:config) do
+ { script: 'rspec', image: 'some_image', cache: { key: 'test' } }
+ end
+
+ it 'overrides global config' do
+ expect(entry[:image].value).to eq 'some_image'
+ expect(entry[:cache].value).to eq(key: 'test')
+ end
+ end
+
+ context 'when job config does not override global config' do
+ before do
+ allow(deps).to receive('[]').with(:image).and_return(specified)
+ entry.compose!(deps)
+ end
+
+ let(:config) { { script: 'ls', cache: { key: 'test' } } }
+
+ it 'uses config from global entry' do
+ expect(entry[:image].value).to eq 'specified'
+ expect(entry[:cache].value).to eq(key: 'test')
+ end
+ end
+ end
+
+ context 'when composed' do
+ before { entry.compose! }
+
+ describe '#value' do
+ before { entry.compose! }
+
+ context 'when entry is correct' do
+ let(:config) do
+ { before_script: %w[ls pwd],
+ script: 'rspec',
+ after_script: %w[cleanup] }
+ end
+
+ it 'returns correct value' do
+ expect(entry.value)
+ .to eq(name: :rspec,
+ before_script: %w[ls pwd],
+ script: %w[rspec],
+ commands: "ls\npwd\nrspec",
+ stage: 'test',
+ after_script: %w[cleanup])
+ end
+ end
+ end
+
+ describe '#commands' do
+ let(:config) do
+ { before_script: %w[ls pwd], script: 'rspec' }
+ end
+
+ it 'returns a string of commands concatenated with new line character' do
+ expect(entry.commands).to eq "ls\npwd\nrspec"
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
new file mode 100644
index 00000000000..aaebf783962
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Jobs do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ before { entry.compose! }
+
+ context 'when entry config value is correct' do
+ let(:config) { { rspec: { script: 'rspec' } } }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ describe '#errors' do
+ context 'incorrect config value type' do
+ let(:config) { ['incorrect'] }
+
+ it 'returns error about incorrect type' do
+ expect(entry.errors)
+ .to include 'jobs config should be a hash'
+ end
+ end
+
+ context 'when job is unspecified' do
+ let(:config) { { rspec: nil } }
+
+ it 'reports error' do
+ expect(entry.errors).to include "rspec config can't be blank"
+ end
+ end
+
+ context 'when no visible jobs present' do
+ let(:config) { { '.hidden'.to_sym => { script: [] } } }
+
+ it 'returns error about no visible jobs defined' do
+ expect(entry.errors)
+ .to include 'jobs config should contain at least one visible job'
+ end
+ end
+ end
+ end
+ end
+
+ context 'when valid job entries composed' do
+ before { entry.compose! }
+
+ let(:config) do
+ { rspec: { script: 'rspec' },
+ spinach: { script: 'spinach' },
+ '.hidden'.to_sym => {} }
+ end
+
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to eq(
+ rspec: { name: :rspec,
+ script: %w[rspec],
+ commands: 'rspec',
+ stage: 'test' },
+ spinach: { name: :spinach,
+ script: %w[spinach],
+ commands: 'spinach',
+ stage: 'test' })
+ end
+ end
+
+ describe '#descendants' do
+ it 'creates valid descendant nodes' do
+ expect(entry.descendants.count).to eq 3
+ expect(entry.descendants.first(2))
+ .to all(be_an_instance_of(Gitlab::Ci::Config::Entry::Job))
+ expect(entry.descendants.last)
+ .to be_an_instance_of(Gitlab::Ci::Config::Entry::Hidden)
+ end
+ end
+
+ describe '#value' do
+ it 'returns value of visible jobs only' do
+ expect(entry.value.keys).to eq [:rspec, :spinach]
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/key_spec.rb b/spec/lib/gitlab/ci/config/entry/key_spec.rb
new file mode 100644
index 00000000000..a55e5b4b8ac
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/key_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Key do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when entry config value is correct' do
+ let(:config) { 'test' }
+
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to eq 'test'
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ let(:config) { [ 'incorrect' ] }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'key config should be a string or symbol'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/paths_spec.rb b/spec/lib/gitlab/ci/config/entry/paths_spec.rb
new file mode 100644
index 00000000000..e60c9aaf661
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/paths_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Paths do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when entry config value is valid' do
+ let(:config) { ['some/file', 'some/path/'] }
+
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not valid' do
+ let(:config) { [ 1 ] }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'paths config should be an array of strings'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/script_spec.rb b/spec/lib/gitlab/ci/config/entry/script_spec.rb
new file mode 100644
index 00000000000..aa99cee2690
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/script_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Script do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when entry config value is correct' do
+ let(:config) { ['ls', 'pwd'] }
+
+ describe '#value' do
+ it 'returns array of strings' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#errors' do
+ it 'does not append errors' do
+ expect(entry.errors).to be_empty
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ let(:config) { 'ls' }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'script config should be an array of strings'
+ end
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/services_spec.rb b/spec/lib/gitlab/ci/config/entry/services_spec.rb
new file mode 100644
index 00000000000..66fad3b6b16
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/services_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Services do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when entry config value is correct' do
+ let(:config) { ['postgres:9.1', 'mysql:5.5'] }
+
+ describe '#value' do
+ it 'returns array of services as is' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ let(:config) { 'ls' }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'services config should be an array of strings'
+ end
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/stage_spec.rb b/spec/lib/gitlab/ci/config/entry/stage_spec.rb
new file mode 100644
index 00000000000..70c8a0a355a
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/stage_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Stage do
+ let(:stage) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when stage config value is correct' do
+ let(:config) { 'build' }
+
+ describe '#value' do
+ it 'returns a stage key' do
+ expect(stage.value).to eq config
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(stage).to be_valid
+ end
+ end
+ end
+
+ context 'when value has a wrong type' do
+ let(:config) { { test: true } }
+
+ it 'reports errors about wrong type' do
+ expect(stage.errors)
+ .to include 'stage config should be a string'
+ end
+ end
+ end
+
+ describe '.default' do
+ it 'returns default stage' do
+ expect(described_class.default).to eq 'test'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/stages_spec.rb b/spec/lib/gitlab/ci/config/entry/stages_spec.rb
new file mode 100644
index 00000000000..182c8d867c7
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/stages_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Stages do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when entry config value is correct' do
+ let(:config) { [:stage1, :stage2] }
+
+ describe '#value' do
+ it 'returns array of stages' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ let(:config) { { test: true } }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'stages config should be an array of strings'
+ end
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+ end
+
+ describe '.default' do
+ it 'returns default stages' do
+ expect(described_class.default).to eq %w[build test deploy]
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/trigger_spec.rb b/spec/lib/gitlab/ci/config/entry/trigger_spec.rb
new file mode 100644
index 00000000000..e4ee44f1274
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/trigger_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Trigger do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when entry config value is valid' do
+ context 'when config is a branch or tag name' do
+ let(:config) { %w[master feature/branch] }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to eq config
+ end
+ end
+ end
+
+ context 'when config is a regexp' do
+ let(:config) { ['/^issue-.*$/'] }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when config is a special keyword' do
+ let(:config) { %w[tags triggers branches] }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+ end
+
+ context 'when entry value is not valid' do
+ let(:config) { [1] }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'trigger config should be an array of strings or regexps'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/undefined_spec.rb b/spec/lib/gitlab/ci/config/entry/undefined_spec.rb
new file mode 100644
index 00000000000..fdf48d84192
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/undefined_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Undefined do
+ let(:entry) { described_class.new }
+
+ describe '#leaf?' do
+ it 'is leaf node' do
+ expect(entry).to be_leaf
+ end
+ end
+
+ describe '#valid?' do
+ it 'is always valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'is does not contain errors' do
+ expect(entry.errors).to be_empty
+ end
+ end
+
+ describe '#value' do
+ it 'returns nil' do
+ expect(entry.value).to eq nil
+ end
+ end
+
+ describe '#relevant?' do
+ it 'is not relevant' do
+ expect(entry.relevant?).to eq false
+ end
+ end
+
+ describe '#specified?' do
+ it 'is not defined' do
+ expect(entry.specified?).to eq false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/unspecified_spec.rb b/spec/lib/gitlab/ci/config/entry/unspecified_spec.rb
new file mode 100644
index 00000000000..66f88fa35b6
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/unspecified_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Unspecified do
+ let(:unspecified) { described_class.new(entry) }
+ let(:entry) { spy('Entry') }
+
+ describe '#valid?' do
+ it 'delegates method to entry' do
+ expect(unspecified.valid?).to eq entry
+ end
+ end
+
+ describe '#errors' do
+ it 'delegates method to entry' do
+ expect(unspecified.errors).to eq entry
+ end
+ end
+
+ describe '#value' do
+ it 'delegates method to entry' do
+ expect(unspecified.value).to eq entry
+ end
+ end
+
+ describe '#specified?' do
+ it 'is always false' do
+ allow(entry).to receive(:specified?).and_return(true)
+
+ expect(unspecified.specified?).to be false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/validatable_spec.rb b/spec/lib/gitlab/ci/config/entry/validatable_spec.rb
new file mode 100644
index 00000000000..d1856801827
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/validatable_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Validatable do
+ let(:entry) { Class.new }
+
+ before do
+ entry.include(described_class)
+ end
+
+ describe '.validator' do
+ before do
+ entry.class_eval do
+ attr_accessor :test_attribute
+
+ validations do
+ validates :test_attribute, presence: true
+ end
+ end
+ end
+
+ it 'returns validator' do
+ expect(entry.validator.superclass)
+ .to be Gitlab::Ci::Config::Entry::Validator
+ end
+
+ it 'returns only one validator to mitigate leaks' do
+ expect { entry.validator }.not_to change { entry.validator }
+ end
+
+ context 'when validating entry instance' do
+ let(:entry_instance) { entry.new }
+
+ context 'when attribute is valid' do
+ before do
+ entry_instance.test_attribute = 'valid'
+ end
+
+ it 'instance of validator is valid' do
+ expect(entry.validator.new(entry_instance)).to be_valid
+ end
+ end
+
+ context 'when attribute is not valid' do
+ before do
+ entry_instance.test_attribute = nil
+ end
+
+ it 'instance of validator is invalid' do
+ expect(entry.validator.new(entry_instance)).to be_invalid
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/validator_spec.rb b/spec/lib/gitlab/ci/config/entry/validator_spec.rb
new file mode 100644
index 00000000000..ad7e6f07d3c
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/validator_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Validator do
+ let(:validator) { Class.new(described_class) }
+ let(:validator_instance) { validator.new(node) }
+ let(:node) { spy('node') }
+
+ before do
+ allow(node).to receive(:key).and_return('node')
+ allow(node).to receive(:ancestors).and_return([])
+ end
+
+ describe 'delegated validator' do
+ before do
+ validator.class_eval do
+ validates :test_attribute, presence: true
+ end
+ end
+
+ context 'when node is valid' do
+ before do
+ allow(node).to receive(:test_attribute).and_return('valid value')
+ end
+
+ it 'validates attribute in node' do
+ expect(node).to receive(:test_attribute)
+ expect(validator_instance).to be_valid
+ end
+
+ it 'returns no errors' do
+ validator_instance.validate
+
+ expect(validator_instance.messages).to be_empty
+ end
+ end
+
+ context 'when node is invalid' do
+ before do
+ allow(node).to receive(:test_attribute).and_return(nil)
+ end
+
+ it 'validates attribute in node' do
+ expect(node).to receive(:test_attribute)
+ expect(validator_instance).to be_invalid
+ end
+
+ it 'returns errors' do
+ validator_instance.validate
+
+ expect(validator_instance.messages)
+ .to include "node test attribute can't be blank"
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
new file mode 100644
index 00000000000..58327d08904
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Variables do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when entry config value is correct' do
+ let(:config) do
+ { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' }
+ end
+
+ describe '#value' do
+ it 'returns hash with key value strings' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#errors' do
+ it 'does not append errors' do
+ expect(entry.errors).to be_empty
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ let(:config) { [ :VAR, 'test' ] }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include /should be a hash of key value pairs/
+ end
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/artifacts_spec.rb b/spec/lib/gitlab/ci/config/node/artifacts_spec.rb
deleted file mode 100644
index c09a0a9c793..00000000000
--- a/spec/lib/gitlab/ci/config/node/artifacts_spec.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Artifacts do
- let(:entry) { described_class.new(config) }
-
- describe 'validation' do
- context 'when entry config value is correct' do
- let(:config) { { paths: %w[public/] } }
-
- describe '#value' do
- it 'returns artifacs configuration' do
- expect(entry.value).to eq config
- end
- end
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
-
- context 'when entry value is not correct' do
- describe '#errors' do
- context 'when value of attribute is invalid' do
- let(:config) { { name: 10 } }
-
- it 'reports error' do
- expect(entry.errors)
- .to include 'artifacts name should be a string'
- end
- end
-
- context 'when there is an unknown key present' do
- let(:config) { { test: 100 } }
-
- it 'reports error' do
- expect(entry.errors)
- .to include 'artifacts config contains unknown keys: test'
- end
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/attributable_spec.rb b/spec/lib/gitlab/ci/config/node/attributable_spec.rb
deleted file mode 100644
index 24d9daafd88..00000000000
--- a/spec/lib/gitlab/ci/config/node/attributable_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Attributable do
- let(:node) { Class.new }
- let(:instance) { node.new }
-
- before do
- node.include(described_class)
-
- node.class_eval do
- attributes :name, :test
- end
- end
-
- context 'config is a hash' do
- before do
- allow(instance)
- .to receive(:config)
- .and_return({ name: 'some name', test: 'some test' })
- end
-
- it 'returns the value of config' do
- expect(instance.name).to eq 'some name'
- expect(instance.test).to eq 'some test'
- end
-
- it 'returns no method error for unknown attributes' do
- expect { instance.unknown }.to raise_error(NoMethodError)
- end
- end
-
- context 'config is not a hash' do
- before do
- allow(instance)
- .to receive(:config)
- .and_return('some test')
- end
-
- it 'returns nil' do
- expect(instance.test).to be_nil
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/boolean_spec.rb b/spec/lib/gitlab/ci/config/node/boolean_spec.rb
deleted file mode 100644
index deafa8bf8a7..00000000000
--- a/spec/lib/gitlab/ci/config/node/boolean_spec.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Boolean do
- let(:entry) { described_class.new(config) }
-
- describe 'validations' do
- context 'when entry config value is valid' do
- let(:config) { false }
-
- describe '#value' do
- it 'returns key value' do
- expect(entry.value).to eq false
- end
- end
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
-
- context 'when entry value is not valid' do
- let(:config) { ['incorrect'] }
-
- describe '#errors' do
- it 'saves errors' do
- expect(entry.errors)
- .to include 'boolean config should be a boolean value'
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/cache_spec.rb b/spec/lib/gitlab/ci/config/node/cache_spec.rb
deleted file mode 100644
index e251210949c..00000000000
--- a/spec/lib/gitlab/ci/config/node/cache_spec.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Cache do
- let(:entry) { described_class.new(config) }
-
- describe 'validations' do
- before { entry.compose! }
-
- context 'when entry config value is correct' do
- let(:config) do
- { key: 'some key',
- untracked: true,
- paths: ['some/path/'] }
- end
-
- describe '#value' do
- it 'returns hash value' do
- expect(entry.value).to eq config
- end
- end
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
-
- context 'when entry value is not correct' do
- describe '#errors' do
- context 'when is not a hash' do
- let(:config) { 'ls' }
-
- it 'reports errors with config value' do
- expect(entry.errors)
- .to include 'cache config should be a hash'
- end
- end
-
- context 'when descendants are invalid' do
- let(:config) { { key: 1 } }
-
- it 'reports error with descendants' do
- expect(entry.errors)
- .to include 'key config should be a string or symbol'
- end
- end
-
- context 'when there is an unknown key present' do
- let(:config) { { invalid: true } }
-
- it 'reports error with descendants' do
- expect(entry.errors)
- .to include 'cache config contains unknown keys: invalid'
- end
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/commands_spec.rb b/spec/lib/gitlab/ci/config/node/commands_spec.rb
deleted file mode 100644
index e373c40706f..00000000000
--- a/spec/lib/gitlab/ci/config/node/commands_spec.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Commands do
- let(:entry) { described_class.new(config) }
-
- context 'when entry config value is an array' do
- let(:config) { ['ls', 'pwd'] }
-
- describe '#value' do
- it 'returns array of strings' do
- expect(entry.value).to eq config
- end
- end
-
- describe '#errors' do
- it 'does not append errors' do
- expect(entry.errors).to be_empty
- end
- end
- end
-
- context 'when entry config value is a string' do
- let(:config) { 'ls' }
-
- describe '#value' do
- it 'returns array with single element' do
- expect(entry.value).to eq ['ls']
- end
- end
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
-
- context 'when entry value is not valid' do
- let(:config) { 1 }
-
- describe '#errors' do
- it 'saves errors' do
- expect(entry.errors)
- .to include 'commands config should be a ' \
- 'string or an array of strings'
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/configurable_spec.rb b/spec/lib/gitlab/ci/config/node/configurable_spec.rb
deleted file mode 100644
index c468ecf957b..00000000000
--- a/spec/lib/gitlab/ci/config/node/configurable_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Configurable do
- let(:node) { Class.new }
-
- before do
- node.include(described_class)
- end
-
- describe 'validations' do
- let(:validator) { node.validator.new(instance) }
-
- before do
- node.class_eval do
- attr_reader :config
-
- def initialize(config)
- @config = config
- end
- end
-
- validator.validate
- end
-
- context 'when node validator is invalid' do
- let(:instance) { node.new('ls') }
-
- it 'returns invalid validator' do
- expect(validator).to be_invalid
- end
- end
-
- context 'when node instance is valid' do
- let(:instance) { node.new(key: 'value') }
-
- it 'returns valid validator' do
- expect(validator).to be_valid
- end
- end
- end
-
- describe 'configured nodes' do
- before do
- node.class_eval do
- node :object, Object, description: 'test object'
- end
- end
-
- describe '.nodes' do
- it 'has valid nodes' do
- expect(node.nodes).to include :object
- end
-
- it 'creates a node factory' do
- expect(node.nodes[:object])
- .to be_an_instance_of Gitlab::Ci::Config::Node::Factory
- end
-
- it 'returns a duplicated factory object' do
- first_factory = node.nodes[:object]
- second_factory = node.nodes[:object]
-
- expect(first_factory).not_to be_equal(second_factory)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/environment_spec.rb b/spec/lib/gitlab/ci/config/node/environment_spec.rb
deleted file mode 100644
index df925ff1afd..00000000000
--- a/spec/lib/gitlab/ci/config/node/environment_spec.rb
+++ /dev/null
@@ -1,217 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Environment do
- let(:entry) { described_class.new(config) }
-
- before { entry.compose! }
-
- context 'when configuration is a string' do
- let(:config) { 'production' }
-
- describe '#string?' do
- it 'is string configuration' do
- expect(entry).to be_string
- end
- end
-
- describe '#hash?' do
- it 'is not hash configuration' do
- expect(entry).not_to be_hash
- end
- end
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
-
- describe '#value' do
- it 'returns valid hash' do
- expect(entry.value).to include(name: 'production')
- end
- end
-
- describe '#name' do
- it 'returns environment name' do
- expect(entry.name).to eq 'production'
- end
- end
-
- describe '#url' do
- it 'returns environment url' do
- expect(entry.url).to be_nil
- end
- end
- end
-
- context 'when configuration is a hash' do
- let(:config) do
- { name: 'development', url: 'https://example.gitlab.com' }
- end
-
- describe '#string?' do
- it 'is not string configuration' do
- expect(entry).not_to be_string
- end
- end
-
- describe '#hash?' do
- it 'is hash configuration' do
- expect(entry).to be_hash
- end
- end
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
-
- describe '#value' do
- it 'returns valid hash' do
- expect(entry.value).to eq config
- end
- end
-
- describe '#name' do
- it 'returns environment name' do
- expect(entry.name).to eq 'development'
- end
- end
-
- describe '#url' do
- it 'returns environment url' do
- expect(entry.url).to eq 'https://example.gitlab.com'
- end
- end
- end
-
- context 'when valid action is used' do
- let(:config) do
- { name: 'production',
- action: 'start' }
- end
-
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
-
- context 'when invalid action is used' do
- let(:config) do
- { name: 'production',
- action: 'invalid' }
- end
-
- describe '#valid?' do
- it 'is not valid' do
- expect(entry).not_to be_valid
- end
- end
-
- describe '#errors' do
- it 'contains error about invalid action' do
- expect(entry.errors)
- .to include 'environment action should be start or stop'
- end
- end
- end
-
- context 'when on_stop is used' do
- let(:config) do
- { name: 'production',
- on_stop: 'close_app' }
- end
-
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
-
- context 'when invalid on_stop is used' do
- let(:config) do
- { name: 'production',
- on_stop: false }
- end
-
- describe '#valid?' do
- it 'is not valid' do
- expect(entry).not_to be_valid
- end
- end
-
- describe '#errors' do
- it 'contains error about invalid action' do
- expect(entry.errors)
- .to include 'environment on stop should be a string'
- end
- end
- end
-
- context 'when variables are used for environment' do
- let(:config) do
- { name: 'review/$CI_BUILD_REF_NAME',
- url: 'https://$CI_BUILD_REF_NAME.review.gitlab.com' }
- end
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
-
- context 'when configuration is invalid' do
- context 'when configuration is an array' do
- let(:config) { ['env'] }
-
- describe '#valid?' do
- it 'is not valid' do
- expect(entry).not_to be_valid
- end
- end
-
- describe '#errors' do
- it 'contains error about invalid type' do
- expect(entry.errors)
- .to include 'environment config should be a hash or a string'
- end
- end
- end
-
- context 'when environment name is not present' do
- let(:config) { { url: 'https://example.gitlab.com' } }
-
- describe '#valid?' do
- it 'is not valid' do
- expect(entry).not_to be_valid
- end
- end
-
- describe '#errors?' do
- it 'contains error about missing environment name' do
- expect(entry.errors)
- .to include "environment name can't be blank"
- end
- end
- end
-
- context 'when invalid URL is used' do
- let(:config) { { name: 'test', url: 'invalid-example.gitlab.com' } }
-
- describe '#valid?' do
- it 'is not valid' do
- expect(entry).not_to be_valid
- end
- end
-
- describe '#errors?' do
- it 'contains error about invalid URL' do
- expect(entry.errors)
- .to include "environment url must be a valid url"
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb
deleted file mode 100644
index a699089c563..00000000000
--- a/spec/lib/gitlab/ci/config/node/factory_spec.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Factory do
- describe '#create!' do
- let(:factory) { described_class.new(node) }
- let(:node) { Gitlab::Ci::Config::Node::Script }
-
- context 'when setting a concrete value' do
- it 'creates entry with valid value' do
- entry = factory
- .value(['ls', 'pwd'])
- .create!
-
- expect(entry.value).to eq ['ls', 'pwd']
- end
-
- context 'when setting description' do
- it 'creates entry with description' do
- entry = factory
- .value(['ls', 'pwd'])
- .with(description: 'test description')
- .create!
-
- expect(entry.value).to eq ['ls', 'pwd']
- expect(entry.description).to eq 'test description'
- end
- end
-
- context 'when setting key' do
- it 'creates entry with custom key' do
- entry = factory
- .value(['ls', 'pwd'])
- .with(key: 'test key')
- .create!
-
- expect(entry.key).to eq 'test key'
- end
- end
-
- context 'when setting a parent' do
- let(:object) { Object.new }
-
- it 'creates entry with valid parent' do
- entry = factory
- .value('ls')
- .with(parent: object)
- .create!
-
- expect(entry.parent).to eq object
- end
- end
- end
-
- context 'when not setting a value' do
- it 'raises error' do
- expect { factory.create! }.to raise_error(
- Gitlab::Ci::Config::Node::Factory::InvalidFactory
- )
- end
- end
-
- context 'when creating entry with nil value' do
- it 'creates an undefined entry' do
- entry = factory
- .value(nil)
- .create!
-
- expect(entry)
- .to be_an_instance_of Gitlab::Ci::Config::Node::Unspecified
- end
- end
-
- context 'when passing metadata' do
- let(:node) { spy('node') }
-
- it 'passes metadata as a parameter' do
- factory
- .value('some value')
- .metadata(some: 'hash')
- .create!
-
- expect(node).to have_received(:new)
- .with('some value', { some: 'hash' })
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb
deleted file mode 100644
index 12232ff7e2f..00000000000
--- a/spec/lib/gitlab/ci/config/node/global_spec.rb
+++ /dev/null
@@ -1,299 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::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
- end
-
- it 'returns a hash' do
- expect(described_class.nodes).to be_a Hash
- end
- end
-
- context 'when hash is valid' do
- context 'when some entries defined' do
- let(:hash) do
- { before_script: ['ls', 'pwd'],
- image: 'ruby:2.2',
- services: ['postgres:9.1', 'mysql:5.5'],
- variables: { VAR: 'value' },
- after_script: ['make clean'],
- stages: ['build', 'pages'],
- cache: { key: 'k', untracked: true, paths: ['public/'] },
- rspec: { script: %w[rspec ls] },
- spinach: { before_script: [], variables: {}, script: 'spinach' } }
- end
-
- describe '#compose!' do
- before { global.compose! }
-
- it 'creates nodes hash' do
- expect(global.descendants).to be_an Array
- end
-
- it 'creates node object for each entry' do
- expect(global.descendants.count).to eq 8
- end
-
- it 'creates node object using valid class' do
- expect(global.descendants.first)
- .to be_an_instance_of Gitlab::Ci::Config::Node::Script
- expect(global.descendants.second)
- .to be_an_instance_of Gitlab::Ci::Config::Node::Image
- end
-
- it 'sets correct description for nodes' do
- expect(global.descendants.first.description)
- .to eq 'Script that will be executed before each job.'
- expect(global.descendants.second.description)
- .to eq 'Docker image that will be used to execute jobs.'
- end
-
- describe '#leaf?' do
- it 'is not leaf' do
- expect(global).not_to be_leaf
- end
- end
- end
-
- context 'when not composed' do
- describe '#before_script' do
- it 'returns nil' do
- expect(global.before_script).to be nil
- end
- end
-
- describe '#leaf?' do
- it 'is leaf' do
- expect(global).to be_leaf
- end
- end
- end
-
- context 'when composed' do
- before { global.compose! }
-
- describe '#errors' do
- it 'has no errors' do
- expect(global.errors).to be_empty
- end
- end
-
- describe '#before_script' do
- it 'returns correct script' do
- expect(global.before_script).to eq ['ls', 'pwd']
- end
- end
-
- describe '#image' do
- it 'returns valid image' do
- expect(global.image).to eq 'ruby:2.2'
- end
- end
-
- describe '#services' do
- it 'returns array of services' do
- expect(global.services).to eq ['postgres:9.1', 'mysql:5.5']
- end
- end
-
- describe '#after_script' do
- it 'returns after script' do
- expect(global.after_script).to eq ['make clean']
- end
- end
-
- describe '#variables' do
- it 'returns variables' do
- expect(global.variables).to eq(VAR: 'value')
- end
- end
-
- describe '#stages' do
- context 'when stages key defined' do
- it 'returns array of stages' do
- expect(global.stages).to eq %w[build pages]
- end
- end
-
- context 'when deprecated types key defined' do
- let(:hash) do
- { types: ['test', 'deploy'],
- rspec: { script: 'rspec' } }
- end
-
- it 'returns array of types as stages' do
- expect(global.stages).to eq %w[test deploy]
- end
- end
- end
-
- describe '#cache' do
- it 'returns cache configuration' do
- expect(global.cache)
- .to eq(key: 'k', untracked: true, paths: ['public/'])
- end
- end
-
- describe '#jobs' do
- it 'returns jobs configuration' do
- expect(global.jobs).to eq(
- rspec: { name: :rspec,
- script: %w[rspec ls],
- before_script: ['ls', 'pwd'],
- commands: "ls\npwd\nrspec\nls",
- image: 'ruby:2.2',
- services: ['postgres:9.1', 'mysql:5.5'],
- stage: 'test',
- cache: { key: 'k', untracked: true, paths: ['public/'] },
- variables: { VAR: 'value' },
- after_script: ['make clean'] },
- spinach: { name: :spinach,
- before_script: [],
- script: %w[spinach],
- commands: 'spinach',
- image: 'ruby:2.2',
- services: ['postgres:9.1', 'mysql:5.5'],
- stage: 'test',
- cache: { key: 'k', untracked: true, paths: ['public/'] },
- variables: {},
- after_script: ['make clean'] },
- )
- end
- end
- end
- end
-
- context 'when most of entires not defined' do
- before { global.compose! }
-
- let(:hash) do
- { cache: { key: 'a' }, rspec: { script: %w[ls] } }
- end
-
- describe '#nodes' do
- it 'instantizes all nodes' do
- expect(global.descendants.count).to eq 8
- end
-
- it 'contains unspecified nodes' do
- expect(global.descendants.first)
- .to be_an_instance_of Gitlab::Ci::Config::Node::Unspecified
- end
- end
-
- describe '#variables' do
- it 'returns default value for variables' do
- expect(global.variables).to eq({})
- end
- end
-
- describe '#stages' do
- it 'returns an array of default stages' do
- expect(global.stages).to eq %w[build test deploy]
- end
- end
-
- describe '#cache' do
- it 'returns correct cache definition' do
- expect(global.cache).to eq(key: 'a')
- end
- end
- end
-
- ##
- # When nodes are specified but not defined, we assume that
- # configuration is valid, and we asume that entry is simply undefined,
- # despite the fact, that key is present. See issue #18775 for more
- # details.
- #
- context 'when entires specified but not defined' do
- before { global.compose! }
-
- let(:hash) do
- { variables: nil, rspec: { script: 'rspec' } }
- end
-
- describe '#variables' do
- it 'undefined entry returns a default value' do
- expect(global.variables).to eq({})
- end
- end
- end
- end
-
- context 'when hash is not valid' do
- before { global.compose! }
-
- let(:hash) do
- { before_script: 'ls' }
- end
-
- describe '#valid?' do
- it 'is not valid' do
- expect(global).not_to be_valid
- end
- end
-
- describe '#errors' do
- it 'reports errors from child nodes' do
- expect(global.errors)
- .to include 'before_script config should be an array of strings'
- end
- end
-
- describe '#before_script' do
- it 'returns nil' do
- expect(global.before_script).to be_nil
- end
- end
- end
-
- context 'when value is not a hash' do
- let(:hash) { [] }
-
- describe '#valid?' do
- it 'is not valid' do
- expect(global).not_to be_valid
- end
- end
-
- describe '#errors' do
- it 'returns error about invalid type' do
- expect(global.errors.first).to match /should be a hash/
- end
- end
- end
-
- describe '#specified?' do
- it 'is concrete entry that is defined' do
- expect(global.specified?).to be true
- end
- end
-
- describe '#[]' do
- before { global.compose! }
-
- let(:hash) do
- { cache: { key: 'a' }, rspec: { script: 'ls' } }
- end
-
- context 'when node exists' do
- it 'returns correct entry' do
- expect(global[:cache])
- .to be_an_instance_of Gitlab::Ci::Config::Node::Cache
- expect(global[:jobs][:rspec][:script].value).to eq ['ls']
- end
- end
-
- context 'when node does not exist' do
- it 'always return unspecified node' do
- expect(global[:some][:unknown][:node])
- .not_to be_specified
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/hidden_spec.rb b/spec/lib/gitlab/ci/config/node/hidden_spec.rb
deleted file mode 100644
index 61e2a554419..00000000000
--- a/spec/lib/gitlab/ci/config/node/hidden_spec.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Hidden do
- let(:entry) { described_class.new(config) }
-
- describe 'validations' do
- context 'when entry config value is correct' do
- let(:config) { [:some, :array] }
-
- describe '#value' do
- it 'returns key value' do
- expect(entry.value).to eq [:some, :array]
- end
- end
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
-
- context 'when entry value is not correct' do
- context 'when config is empty' do
- let(:config) { {} }
-
- describe '#valid' do
- it 'is invalid' do
- expect(entry).not_to be_valid
- end
- end
- end
- end
- end
-
- describe '#leaf?' do
- it 'is a leaf' do
- expect(entry).to be_leaf
- end
- end
-
- describe '#relevant?' do
- it 'is not a relevant entry' do
- expect(entry).not_to be_relevant
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/image_spec.rb b/spec/lib/gitlab/ci/config/node/image_spec.rb
deleted file mode 100644
index d11bb39f328..00000000000
--- a/spec/lib/gitlab/ci/config/node/image_spec.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Image do
- let(:entry) { described_class.new(config) }
-
- describe 'validation' do
- context 'when entry config value is correct' do
- let(:config) { 'ruby:2.2' }
-
- describe '#value' do
- it 'returns image string' do
- expect(entry.value).to eq 'ruby:2.2'
- end
- end
-
- describe '#errors' do
- it 'does not append errors' do
- expect(entry.errors).to be_empty
- end
- end
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
-
- context 'when entry value is not correct' do
- let(:config) { ['ruby:2.2'] }
-
- describe '#errors' do
- it 'saves errors' do
- expect(entry.errors)
- .to include 'image config should be a string'
- end
- end
-
- describe '#valid?' do
- it 'is not valid' do
- expect(entry).not_to be_valid
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb
deleted file mode 100644
index 91f676dae03..00000000000
--- a/spec/lib/gitlab/ci/config/node/job_spec.rb
+++ /dev/null
@@ -1,140 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Job do
- let(:entry) { described_class.new(config, name: :rspec) }
-
- describe 'validations' do
- before { entry.compose! }
-
- context 'when entry config value is correct' do
- let(:config) { { script: 'rspec' } }
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
-
- context 'when job name is empty' do
- let(:entry) { described_class.new(config, name: ''.to_sym) }
-
- it 'reports error' do
- expect(entry.errors)
- .to include "job name can't be blank"
- end
- end
- end
-
- context 'when entry value is not correct' do
- context 'incorrect config value type' do
- let(:config) { ['incorrect'] }
-
- describe '#errors' do
- it 'reports error about a config type' do
- expect(entry.errors)
- .to include 'job config should be a hash'
- end
- end
- end
-
- context 'when config is empty' do
- let(:config) { {} }
-
- describe '#valid' do
- it 'is invalid' do
- expect(entry).not_to be_valid
- end
- end
- end
-
- context 'when unknown keys detected' do
- let(:config) { { unknown: true } }
-
- describe '#valid' do
- it 'is not valid' do
- expect(entry).not_to be_valid
- end
- end
- end
- end
- end
-
- describe '#relevant?' do
- it 'is a relevant entry' do
- expect(entry).to be_relevant
- end
- end
-
- describe '#compose!' do
- let(:unspecified) { double('unspecified', 'specified?' => false) }
-
- let(:specified) do
- double('specified', 'specified?' => true, value: 'specified')
- end
-
- let(:deps) { double('deps', '[]' => unspecified) }
-
- context 'when job config overrides global config' do
- before { entry.compose!(deps) }
-
- let(:config) do
- { image: 'some_image', cache: { key: 'test' } }
- end
-
- it 'overrides global config' do
- expect(entry[:image].value).to eq 'some_image'
- expect(entry[:cache].value).to eq(key: 'test')
- end
- end
-
- context 'when job config does not override global config' do
- before do
- allow(deps).to receive('[]').with(:image).and_return(specified)
- entry.compose!(deps)
- end
-
- let(:config) { { script: 'ls', cache: { key: 'test' } } }
-
- it 'uses config from global entry' do
- expect(entry[:image].value).to eq 'specified'
- expect(entry[:cache].value).to eq(key: 'test')
- end
- end
- end
-
- context 'when composed' do
- before { entry.compose! }
-
- describe '#value' do
- before { entry.compose! }
-
- context 'when entry is correct' do
- let(:config) do
- { before_script: %w[ls pwd],
- script: 'rspec',
- after_script: %w[cleanup] }
- end
-
- it 'returns correct value' do
- expect(entry.value)
- .to eq(name: :rspec,
- before_script: %w[ls pwd],
- script: %w[rspec],
- commands: "ls\npwd\nrspec",
- stage: 'test',
- after_script: %w[cleanup])
- end
- end
- end
-
- describe '#commands' do
- let(:config) do
- { before_script: %w[ls pwd], script: 'rspec' }
- end
-
- it 'returns a string of commands concatenated with new line character' do
- expect(entry.commands).to eq "ls\npwd\nrspec"
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb
deleted file mode 100644
index 929809339ef..00000000000
--- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Jobs do
- let(:entry) { described_class.new(config) }
-
- describe 'validations' do
- before { entry.compose! }
-
- context 'when entry config value is correct' do
- let(:config) { { rspec: { script: 'rspec' } } }
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
-
- context 'when entry value is not correct' do
- describe '#errors' do
- context 'incorrect config value type' do
- let(:config) { ['incorrect'] }
-
- it 'returns error about incorrect type' do
- expect(entry.errors)
- .to include 'jobs config should be a hash'
- end
- end
-
- context 'when job is unspecified' do
- let(:config) { { rspec: nil } }
-
- it 'reports error' do
- expect(entry.errors).to include "rspec config can't be blank"
- end
- end
-
- context 'when no visible jobs present' do
- let(:config) { { '.hidden'.to_sym => { script: [] } } }
-
- it 'returns error about no visible jobs defined' do
- expect(entry.errors)
- .to include 'jobs config should contain at least one visible job'
- end
- end
- end
- end
- end
-
- context 'when valid job entries composed' do
- before { entry.compose! }
-
- let(:config) do
- { rspec: { script: 'rspec' },
- spinach: { script: 'spinach' },
- '.hidden'.to_sym => {} }
- end
-
- describe '#value' do
- it 'returns key value' do
- expect(entry.value).to eq(
- rspec: { name: :rspec,
- script: %w[rspec],
- commands: 'rspec',
- stage: 'test' },
- spinach: { name: :spinach,
- script: %w[spinach],
- commands: 'spinach',
- stage: 'test' })
- end
- end
-
- describe '#descendants' do
- it 'creates valid descendant nodes' do
- expect(entry.descendants.count).to eq 3
- expect(entry.descendants.first(2))
- .to all(be_an_instance_of(Gitlab::Ci::Config::Node::Job))
- expect(entry.descendants.last)
- .to be_an_instance_of(Gitlab::Ci::Config::Node::Hidden)
- end
- end
-
- describe '#value' do
- it 'returns value of visible jobs only' do
- expect(entry.value.keys).to eq [:rspec, :spinach]
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/key_spec.rb b/spec/lib/gitlab/ci/config/node/key_spec.rb
deleted file mode 100644
index 8cda43173fe..00000000000
--- a/spec/lib/gitlab/ci/config/node/key_spec.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Key do
- let(:entry) { described_class.new(config) }
-
- describe 'validations' do
- context 'when entry config value is correct' do
- let(:config) { 'test' }
-
- describe '#value' do
- it 'returns key value' do
- expect(entry.value).to eq 'test'
- end
- end
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
-
- context 'when entry value is not correct' do
- let(:config) { [ 'incorrect' ] }
-
- describe '#errors' do
- it 'saves errors' do
- expect(entry.errors)
- .to include 'key config should be a string or symbol'
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/paths_spec.rb b/spec/lib/gitlab/ci/config/node/paths_spec.rb
deleted file mode 100644
index 6fd744b3975..00000000000
--- a/spec/lib/gitlab/ci/config/node/paths_spec.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Paths do
- let(:entry) { described_class.new(config) }
-
- describe 'validations' do
- context 'when entry config value is valid' do
- let(:config) { ['some/file', 'some/path/'] }
-
- describe '#value' do
- it 'returns key value' do
- expect(entry.value).to eq config
- end
- end
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
-
- context 'when entry value is not valid' do
- let(:config) { [ 1 ] }
-
- describe '#errors' do
- it 'saves errors' do
- expect(entry.errors)
- .to include 'paths config should be an array of strings'
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/script_spec.rb b/spec/lib/gitlab/ci/config/node/script_spec.rb
deleted file mode 100644
index 219a7e981d3..00000000000
--- a/spec/lib/gitlab/ci/config/node/script_spec.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Script do
- let(:entry) { described_class.new(config) }
-
- describe 'validations' do
- context 'when entry config value is correct' do
- let(:config) { ['ls', 'pwd'] }
-
- describe '#value' do
- it 'returns array of strings' do
- expect(entry.value).to eq config
- end
- end
-
- describe '#errors' do
- it 'does not append errors' do
- expect(entry.errors).to be_empty
- end
- end
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
-
- context 'when entry value is not correct' do
- let(:config) { 'ls' }
-
- describe '#errors' do
- it 'saves errors' do
- expect(entry.errors)
- .to include 'script config should be an array of strings'
- end
- end
-
- describe '#valid?' do
- it 'is not valid' do
- expect(entry).not_to be_valid
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/services_spec.rb b/spec/lib/gitlab/ci/config/node/services_spec.rb
deleted file mode 100644
index be0fe46befd..00000000000
--- a/spec/lib/gitlab/ci/config/node/services_spec.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Services do
- let(:entry) { described_class.new(config) }
-
- describe 'validations' do
- context 'when entry config value is correct' do
- let(:config) { ['postgres:9.1', 'mysql:5.5'] }
-
- describe '#value' do
- it 'returns array of services as is' do
- expect(entry.value).to eq config
- end
- end
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
-
- context 'when entry value is not correct' do
- let(:config) { 'ls' }
-
- describe '#errors' do
- it 'saves errors' do
- expect(entry.errors)
- .to include 'services config should be an array of strings'
- end
- end
-
- describe '#valid?' do
- it 'is not valid' do
- expect(entry).not_to be_valid
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/stage_spec.rb b/spec/lib/gitlab/ci/config/node/stage_spec.rb
deleted file mode 100644
index fb9ec70762a..00000000000
--- a/spec/lib/gitlab/ci/config/node/stage_spec.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Stage do
- let(:stage) { described_class.new(config) }
-
- describe 'validations' do
- context 'when stage config value is correct' do
- let(:config) { 'build' }
-
- describe '#value' do
- it 'returns a stage key' do
- expect(stage.value).to eq config
- end
- end
-
- describe '#valid?' do
- it 'is valid' do
- expect(stage).to be_valid
- end
- end
- end
-
- context 'when value has a wrong type' do
- let(:config) { { test: true } }
-
- it 'reports errors about wrong type' do
- expect(stage.errors)
- .to include 'stage config should be a string'
- end
- end
- end
-
- describe '.default' do
- it 'returns default stage' do
- expect(described_class.default).to eq 'test'
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/stages_spec.rb b/spec/lib/gitlab/ci/config/node/stages_spec.rb
deleted file mode 100644
index 1a3818d8997..00000000000
--- a/spec/lib/gitlab/ci/config/node/stages_spec.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Stages do
- let(:entry) { described_class.new(config) }
-
- describe 'validations' do
- context 'when entry config value is correct' do
- let(:config) { [:stage1, :stage2] }
-
- describe '#value' do
- it 'returns array of stages' do
- expect(entry.value).to eq config
- end
- end
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
-
- context 'when entry value is not correct' do
- let(:config) { { test: true } }
-
- describe '#errors' do
- it 'saves errors' do
- expect(entry.errors)
- .to include 'stages config should be an array of strings'
- end
- end
-
- describe '#valid?' do
- it 'is not valid' do
- expect(entry).not_to be_valid
- end
- end
- end
- end
-
- describe '.default' do
- it 'returns default stages' do
- expect(described_class.default).to eq %w[build test deploy]
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/trigger_spec.rb b/spec/lib/gitlab/ci/config/node/trigger_spec.rb
deleted file mode 100644
index a4a3e36754e..00000000000
--- a/spec/lib/gitlab/ci/config/node/trigger_spec.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Trigger do
- let(:entry) { described_class.new(config) }
-
- describe 'validations' do
- context 'when entry config value is valid' do
- context 'when config is a branch or tag name' do
- let(:config) { %w[master feature/branch] }
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
-
- describe '#value' do
- it 'returns key value' do
- expect(entry.value).to eq config
- end
- end
- end
-
- context 'when config is a regexp' do
- let(:config) { ['/^issue-.*$/'] }
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
-
- context 'when config is a special keyword' do
- let(:config) { %w[tags triggers branches] }
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
- end
-
- context 'when entry value is not valid' do
- let(:config) { [1] }
-
- describe '#errors' do
- it 'saves errors' do
- expect(entry.errors)
- .to include 'trigger config should be an array of strings or regexps'
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/undefined_spec.rb b/spec/lib/gitlab/ci/config/node/undefined_spec.rb
deleted file mode 100644
index 6bde8602963..00000000000
--- a/spec/lib/gitlab/ci/config/node/undefined_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Undefined do
- let(:entry) { described_class.new }
-
- describe '#leaf?' do
- it 'is leaf node' do
- expect(entry).to be_leaf
- end
- end
-
- describe '#valid?' do
- it 'is always valid' do
- expect(entry).to be_valid
- end
- end
-
- describe '#errors' do
- it 'is does not contain errors' do
- expect(entry.errors).to be_empty
- end
- end
-
- describe '#value' do
- it 'returns nil' do
- expect(entry.value).to eq nil
- end
- end
-
- describe '#relevant?' do
- it 'is not relevant' do
- expect(entry.relevant?).to eq false
- end
- end
-
- describe '#specified?' do
- it 'is not defined' do
- expect(entry.specified?).to eq false
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/unspecified_spec.rb b/spec/lib/gitlab/ci/config/node/unspecified_spec.rb
deleted file mode 100644
index ba3ceef24ce..00000000000
--- a/spec/lib/gitlab/ci/config/node/unspecified_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Unspecified do
- let(:unspecified) { described_class.new(entry) }
- let(:entry) { spy('Entry') }
-
- describe '#valid?' do
- it 'delegates method to entry' do
- expect(unspecified.valid?).to eq entry
- end
- end
-
- describe '#errors' do
- it 'delegates method to entry' do
- expect(unspecified.errors).to eq entry
- end
- end
-
- describe '#value' do
- it 'delegates method to entry' do
- expect(unspecified.value).to eq entry
- end
- end
-
- describe '#specified?' do
- it 'is always false' do
- allow(entry).to receive(:specified?).and_return(true)
-
- expect(unspecified.specified?).to be false
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/validatable_spec.rb b/spec/lib/gitlab/ci/config/node/validatable_spec.rb
deleted file mode 100644
index 64b77fd6e03..00000000000
--- a/spec/lib/gitlab/ci/config/node/validatable_spec.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Validatable do
- let(:node) { Class.new }
-
- before do
- node.include(described_class)
- end
-
- describe '.validator' do
- before do
- node.class_eval do
- attr_accessor :test_attribute
-
- validations do
- validates :test_attribute, presence: true
- end
- end
- end
-
- it 'returns validator' do
- expect(node.validator.superclass)
- .to be Gitlab::Ci::Config::Node::Validator
- end
-
- it 'returns only one validator to mitigate leaks' do
- expect { node.validator }.not_to change { node.validator }
- end
-
- context 'when validating node instance' do
- let(:node_instance) { node.new }
-
- context 'when attribute is valid' do
- before do
- node_instance.test_attribute = 'valid'
- end
-
- it 'instance of validator is valid' do
- expect(node.validator.new(node_instance)).to be_valid
- end
- end
-
- context 'when attribute is not valid' do
- before do
- node_instance.test_attribute = nil
- end
-
- it 'instance of validator is invalid' do
- expect(node.validator.new(node_instance)).to be_invalid
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/validator_spec.rb b/spec/lib/gitlab/ci/config/node/validator_spec.rb
deleted file mode 100644
index 090fd63b844..00000000000
--- a/spec/lib/gitlab/ci/config/node/validator_spec.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Validator do
- let(:validator) { Class.new(described_class) }
- let(:validator_instance) { validator.new(node) }
- let(:node) { spy('node') }
-
- before do
- allow(node).to receive(:key).and_return('node')
- allow(node).to receive(:ancestors).and_return([])
- end
-
- describe 'delegated validator' do
- before do
- validator.class_eval do
- validates :test_attribute, presence: true
- end
- end
-
- context 'when node is valid' do
- before do
- allow(node).to receive(:test_attribute).and_return('valid value')
- end
-
- it 'validates attribute in node' do
- expect(node).to receive(:test_attribute)
- expect(validator_instance).to be_valid
- end
-
- it 'returns no errors' do
- validator_instance.validate
-
- expect(validator_instance.messages).to be_empty
- end
- end
-
- context 'when node is invalid' do
- before do
- allow(node).to receive(:test_attribute).and_return(nil)
- end
-
- it 'validates attribute in node' do
- expect(node).to receive(:test_attribute)
- expect(validator_instance).to be_invalid
- end
-
- it 'returns errors' do
- validator_instance.validate
-
- expect(validator_instance.messages)
- .to include "node test attribute can't be blank"
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/variables_spec.rb b/spec/lib/gitlab/ci/config/node/variables_spec.rb
deleted file mode 100644
index 4b6d971ec71..00000000000
--- a/spec/lib/gitlab/ci/config/node/variables_spec.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Variables do
- let(:entry) { described_class.new(config) }
-
- describe 'validations' do
- context 'when entry config value is correct' do
- let(:config) do
- { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' }
- end
-
- describe '#value' do
- it 'returns hash with key value strings' do
- expect(entry.value).to eq config
- end
- end
-
- describe '#errors' do
- it 'does not append errors' do
- expect(entry.errors).to be_empty
- end
- end
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
-
- context 'when entry value is not correct' do
- let(:config) { [ :VAR, 'test' ] }
-
- describe '#errors' do
- it 'saves errors' do
- expect(entry.errors)
- .to include /should be a hash of key value pairs/
- end
- end
-
- describe '#valid?' do
- it 'is not valid' do
- expect(entry).not_to be_valid
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
new file mode 100644
index 00000000000..9376bce17a1
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
@@ -0,0 +1,86 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Cancelable do
+ let(:status) { double('core status') }
+ let(:user) { double('user') }
+
+ subject do
+ described_class.new(status)
+ end
+
+ describe '#text' do
+ it 'does not override status text' do
+ expect(status).to receive(:text)
+
+ subject.text
+ end
+ end
+
+ describe '#icon' do
+ it 'does not override status icon' do
+ expect(status).to receive(:icon)
+
+ subject.icon
+ end
+ end
+
+ describe '#label' do
+ it 'does not override status label' do
+ expect(status).to receive(:label)
+
+ subject.label
+ end
+ end
+
+ describe 'action details' do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build) }
+ let(:status) { Gitlab::Ci::Status::Core.new(build, user) }
+
+ describe '#has_action?' do
+ context 'when user is allowed to update build' do
+ before { build.project.team << [user, :developer] }
+
+ it { is_expected.to have_action }
+ end
+
+ context 'when user is not allowed to update build' do
+ it { is_expected.not_to have_action }
+ end
+ end
+
+ describe '#action_path' do
+ it { expect(subject.action_path).to include "#{build.id}/cancel" }
+ end
+
+ describe '#action_icon' do
+ it { expect(subject.action_icon).to eq 'ban' }
+ end
+
+ describe '#action_title' do
+ it { expect(subject.action_title).to eq 'Cancel' }
+ end
+ end
+
+ describe '.matches?' do
+ subject { described_class.matches?(build, user) }
+
+ context 'when build is cancelable' do
+ let(:build) do
+ create(:ci_build, :running)
+ end
+
+ it 'is a correct match' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'when build is not cancelable' do
+ let(:build) { create(:ci_build, :success) }
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/common_spec.rb b/spec/lib/gitlab/ci/status/build/common_spec.rb
new file mode 100644
index 00000000000..40b96b1807b
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/common_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Common do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build) }
+ let(:project) { build.project }
+
+ subject do
+ Gitlab::Ci::Status::Core
+ .new(build, user)
+ .extend(described_class)
+ end
+
+ describe '#has_action?' do
+ it { is_expected.not_to have_action }
+ end
+
+ describe '#has_details?' do
+ context 'when user has access to read build' do
+ before { project.team << [user, :developer] }
+
+ it { is_expected.to have_details }
+ end
+
+ context 'when user does not have access to read build' do
+ before { project.update(public_builds: false) }
+
+ it { is_expected.not_to have_details }
+ end
+ end
+
+ describe '#details_path' do
+ it 'links to the build details page' do
+ expect(subject.details_path).to include "builds/#{build.id}"
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
new file mode 100644
index 00000000000..dccb29b5ef6
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -0,0 +1,141 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Factory do
+ let(:user) { create(:user) }
+ let(:project) { build.project }
+
+ subject { described_class.new(build, user) }
+ let(:status) { subject.fabricate! }
+
+ before { project.team << [user, :developer] }
+
+ context 'when build is successful' do
+ let(:build) { create(:ci_build, :success) }
+
+ it 'fabricates a retryable build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'passed'
+ expect(status.icon).to eq 'icon_status_success'
+ expect(status.label).to eq 'passed'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+
+ context 'when build is failed' do
+ let(:build) { create(:ci_build, :failed) }
+
+ it 'fabricates a retryable build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'failed'
+ expect(status.icon).to eq 'icon_status_failed'
+ expect(status.label).to eq 'failed'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+
+ context 'when build is a canceled' do
+ let(:build) { create(:ci_build, :canceled) }
+
+ it 'fabricates a retryable build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'canceled'
+ expect(status.icon).to eq 'icon_status_canceled'
+ expect(status.label).to eq 'canceled'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+
+ context 'when build is running' do
+ let(:build) { create(:ci_build, :running) }
+
+ it 'fabricates a canceable build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'running'
+ expect(status.icon).to eq 'icon_status_running'
+ expect(status.label).to eq 'running'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+
+ context 'when build is pending' do
+ let(:build) { create(:ci_build, :pending) }
+
+ it 'fabricates a cancelable build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'pending'
+ expect(status.icon).to eq 'icon_status_pending'
+ expect(status.label).to eq 'pending'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+
+ context 'when build is skipped' do
+ let(:build) { create(:ci_build, :skipped) }
+
+ it 'fabricates a core skipped status' do
+ expect(status).to be_a Gitlab::Ci::Status::Skipped
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'skipped'
+ expect(status.icon).to eq 'icon_status_skipped'
+ expect(status.label).to eq 'skipped'
+ expect(status).to have_details
+ expect(status).not_to have_action
+ end
+ end
+
+ context 'when build is a manual action' do
+ context 'when build is a play action' do
+ let(:build) { create(:ci_build, :playable) }
+
+ it 'fabricates a core skipped status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Play
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'manual'
+ expect(status.icon).to eq 'icon_status_manual'
+ expect(status.label).to eq 'manual play action'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+
+ context 'when build is an environment stop action' do
+ let(:build) { create(:ci_build, :playable, :teardown_environment) }
+
+ it 'fabricates a core skipped status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Stop
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'manual'
+ expect(status.icon).to eq 'icon_status_manual'
+ expect(status.label).to eq 'manual stop action'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb
new file mode 100644
index 00000000000..4ddf04a8e11
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/play_spec.rb
@@ -0,0 +1,82 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Play do
+ let(:status) { double('core') }
+ let(:user) { double('user') }
+
+ subject { described_class.new(status) }
+
+ describe '#text' do
+ it { expect(subject.text).to eq 'manual' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'manual play action' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'icon_status_manual' }
+ end
+
+ describe 'action details' do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build) }
+ let(:status) { Gitlab::Ci::Status::Core.new(build, user) }
+
+ describe '#has_action?' do
+ context 'when user is allowed to update build' do
+ before { build.project.team << [user, :developer] }
+
+ it { is_expected.to have_action }
+ end
+
+ context 'when user is not allowed to update build' do
+ it { is_expected.not_to have_action }
+ end
+ end
+
+ describe '#action_path' do
+ it { expect(subject.action_path).to include "#{build.id}/play" }
+ end
+
+ describe '#action_icon' do
+ it { expect(subject.action_icon).to eq 'play' }
+ end
+
+ describe '#action_title' do
+ it { expect(subject.action_title).to eq 'Play' }
+ end
+ end
+
+ describe '.matches?' do
+ subject { described_class.matches?(build, user) }
+
+ context 'when build is playable' do
+ context 'when build stops an environment' do
+ let(:build) do
+ create(:ci_build, :playable, :teardown_environment)
+ end
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+
+ context 'when build does not stop an environment' do
+ let(:build) { create(:ci_build, :playable) }
+
+ it 'is a correct match' do
+ expect(subject).to be true
+ end
+ end
+ end
+
+ context 'when build is not playable' do
+ let(:build) { create(:ci_build) }
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb
new file mode 100644
index 00000000000..d61e5bbaa6b
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb
@@ -0,0 +1,86 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Retryable do
+ let(:status) { double('core status') }
+ let(:user) { double('user') }
+
+ subject do
+ described_class.new(status)
+ end
+
+ describe '#text' do
+ it 'does not override status text' do
+ expect(status).to receive(:text)
+
+ subject.text
+ end
+ end
+
+ describe '#icon' do
+ it 'does not override status icon' do
+ expect(status).to receive(:icon)
+
+ subject.icon
+ end
+ end
+
+ describe '#label' do
+ it 'does not override status label' do
+ expect(status).to receive(:label)
+
+ subject.label
+ end
+ end
+
+ describe 'action details' do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build) }
+ let(:status) { Gitlab::Ci::Status::Core.new(build, user) }
+
+ describe '#has_action?' do
+ context 'when user is allowed to update build' do
+ before { build.project.team << [user, :developer] }
+
+ it { is_expected.to have_action }
+ end
+
+ context 'when user is not allowed to update build' do
+ it { is_expected.not_to have_action }
+ end
+ end
+
+ describe '#action_path' do
+ it { expect(subject.action_path).to include "#{build.id}/retry" }
+ end
+
+ describe '#action_icon' do
+ it { expect(subject.action_icon).to eq 'refresh' }
+ end
+
+ describe '#action_title' do
+ it { expect(subject.action_title).to eq 'Retry' }
+ end
+ end
+
+ describe '.matches?' do
+ subject { described_class.matches?(build, user) }
+
+ context 'when build is retryable' do
+ let(:build) do
+ create(:ci_build, :success)
+ end
+
+ it 'is a correct match' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'when build is not retryable' do
+ let(:build) { create(:ci_build, :running) }
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb
new file mode 100644
index 00000000000..59a85b55f90
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb
@@ -0,0 +1,84 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Stop do
+ let(:status) { double('core status') }
+ let(:user) { double('user') }
+
+ subject do
+ described_class.new(status)
+ end
+
+ describe '#text' do
+ it { expect(subject.text).to eq 'manual' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'manual stop action' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'icon_status_manual' }
+ end
+
+ describe 'action details' do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build) }
+ let(:status) { Gitlab::Ci::Status::Core.new(build, user) }
+
+ describe '#has_action?' do
+ context 'when user is allowed to update build' do
+ before { build.project.team << [user, :developer] }
+
+ it { is_expected.to have_action }
+ end
+
+ context 'when user is not allowed to update build' do
+ it { is_expected.not_to have_action }
+ end
+ end
+
+ describe '#action_path' do
+ it { expect(subject.action_path).to include "#{build.id}/play" }
+ end
+
+ describe '#action_icon' do
+ it { expect(subject.action_icon).to eq 'stop' }
+ end
+
+ describe '#action_title' do
+ it { expect(subject.action_title).to eq 'Stop' }
+ end
+ end
+
+ describe '.matches?' do
+ subject { described_class.matches?(build, user) }
+
+ context 'when build is playable' do
+ context 'when build stops an environment' do
+ let(:build) do
+ create(:ci_build, :playable, :teardown_environment)
+ end
+
+ it 'is a correct match' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'when build does not stop an environment' do
+ let(:build) { create(:ci_build, :playable) }
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+ end
+
+ context 'when build is not playable' do
+ let(:build) { create(:ci_build) }
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb
new file mode 100644
index 00000000000..4639278ad45
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/canceled_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Canceled do
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
+
+ describe '#text' do
+ it { expect(subject.label).to eq 'canceled' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'canceled' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'icon_status_canceled' }
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb
new file mode 100644
index 00000000000..2ce176a29d6
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/created_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Created do
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
+
+ describe '#text' do
+ it { expect(subject.label).to eq 'created' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'created' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'icon_status_created' }
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/extended_spec.rb b/spec/lib/gitlab/ci/status/extended_spec.rb
new file mode 100644
index 00000000000..c2d74ca5cde
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/extended_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Extended do
+ subject do
+ Class.new.include(described_class)
+ end
+
+ it 'requires subclass to implement matcher' do
+ expect { subject.matches?(double, double) }
+ .to raise_error(NotImplementedError)
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb
new file mode 100644
index 00000000000..f92a1c149bf
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/factory_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Factory do
+ subject do
+ described_class.new(resource, user)
+ end
+
+ let(:user) { create(:user) }
+
+ let(:status) { subject.fabricate! }
+
+ context 'when object has a core status' do
+ HasStatus::AVAILABLE_STATUSES.each do |core_status|
+ context "when core status is #{core_status}" do
+ let(:resource) { double(status: core_status) }
+
+ it "fabricates a core status #{core_status}" do
+ expect(status).to be_a(
+ Gitlab::Ci::Status.const_get(core_status.capitalize))
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb
new file mode 100644
index 00000000000..9d527e6a7ef
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/failed_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Failed do
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
+
+ describe '#text' do
+ it { expect(subject.label).to eq 'failed' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'failed' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'icon_status_failed' }
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb
new file mode 100644
index 00000000000..d03f595d3c7
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/pending_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Pending do
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
+
+ describe '#text' do
+ it { expect(subject.label).to eq 'pending' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'pending' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'icon_status_pending' }
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb
new file mode 100644
index 00000000000..d665674bf70
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Pipeline::Common do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :private) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ subject do
+ Gitlab::Ci::Status::Core
+ .new(pipeline, user)
+ .extend(described_class)
+ end
+
+ describe '#has_action?' do
+ it { is_expected.not_to have_action }
+ end
+
+ describe '#has_details?' do
+ context 'when user has access to read pipeline' do
+ before { project.team << [user, :developer] }
+
+ it { is_expected.to have_details }
+ end
+
+ context 'when user does not have access to read pipeline' do
+ it { is_expected.not_to have_details }
+ end
+ end
+
+ describe '#details_path' do
+ it 'links to the pipeline details page' do
+ expect(subject.details_path)
+ .to include "pipelines/#{pipeline.id}"
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
new file mode 100644
index 00000000000..d4a2dc7fcc1
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Pipeline::Factory do
+ let(:user) { create(:user) }
+ let(:project) { pipeline.project }
+
+ subject do
+ described_class.new(pipeline, user)
+ end
+
+ let(:status) do
+ subject.fabricate!
+ end
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'when pipeline has a core status' do
+ HasStatus::AVAILABLE_STATUSES.each do |core_status|
+ context "when core status is #{core_status}" do
+ let(:pipeline) do
+ create(:ci_pipeline, status: core_status)
+ end
+
+ it "fabricates a core status #{core_status}" do
+ expect(status).to be_a(
+ Gitlab::Ci::Status.const_get(core_status.capitalize))
+ end
+
+ it 'extends core status with common pipeline methods' do
+ expect(status).to have_details
+ expect(status).not_to have_action
+ expect(status.details_path)
+ .to include "pipelines/#{pipeline.id}"
+ end
+ end
+ end
+ end
+
+ context 'when pipeline has warnings' do
+ let(:pipeline) do
+ create(:ci_pipeline, status: :success)
+ end
+
+ before do
+ create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline)
+ end
+
+ it 'fabricates extended "success with warnings" status' do
+ expect(status)
+ .to be_a Gitlab::Ci::Status::Pipeline::SuccessWithWarnings
+ end
+
+ it 'extends core status with common pipeline methods' do
+ expect(status).to have_details
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb
new file mode 100644
index 00000000000..7e3383c307f
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do
+ subject do
+ described_class.new(double('status'))
+ end
+
+ describe '#test' do
+ it { expect(subject.text).to eq 'passed' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'passed with warnings' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'icon_status_warning' }
+ end
+
+ describe '.matches?' do
+ context 'when pipeline is successful' do
+ let(:pipeline) do
+ create(:ci_pipeline, status: :success)
+ end
+
+ context 'when pipeline has warnings' do
+ before do
+ allow(pipeline).to receive(:has_warnings?).and_return(true)
+ end
+
+ it 'is a correct match' do
+ expect(described_class.matches?(pipeline, double)).to eq true
+ end
+ end
+
+ context 'when pipeline does not have warnings' do
+ it 'does not match' do
+ expect(described_class.matches?(pipeline, double)).to eq false
+ end
+ end
+ end
+
+ context 'when pipeline is not successful' do
+ let(:pipeline) do
+ create(:ci_pipeline, status: :skipped)
+ end
+
+ context 'when pipeline has warnings' do
+ before do
+ allow(pipeline).to receive(:has_warnings?).and_return(true)
+ end
+
+ it 'does not match' do
+ expect(described_class.matches?(pipeline, double)).to eq false
+ end
+ end
+
+ context 'when pipeline does not have warnings' do
+ it 'does not match' do
+ expect(described_class.matches?(pipeline, double)).to eq false
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb
new file mode 100644
index 00000000000..9f47090d396
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/running_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Running do
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
+
+ describe '#text' do
+ it { expect(subject.label).to eq 'running' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'running' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'icon_status_running' }
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb
new file mode 100644
index 00000000000..94601648a8d
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/skipped_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Skipped do
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
+
+ describe '#text' do
+ it { expect(subject.label).to eq 'skipped' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'skipped' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'icon_status_skipped' }
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/stage/common_spec.rb b/spec/lib/gitlab/ci/status/stage/common_spec.rb
new file mode 100644
index 00000000000..8814a7614a0
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/stage/common_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Stage::Common do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ let(:stage) do
+ build(:ci_stage, pipeline: pipeline, name: 'test')
+ end
+
+ subject do
+ Class.new(Gitlab::Ci::Status::Core)
+ .new(stage, user).extend(described_class)
+ end
+
+ it 'does not have action' do
+ expect(subject).not_to have_action
+ end
+
+ it 'links to the pipeline details page' do
+ expect(subject.details_path)
+ .to include "pipelines/#{pipeline.id}"
+ expect(subject.details_path)
+ .to include "##{stage.name}"
+ end
+
+ context 'when user has permission to read pipeline' do
+ before do
+ project.team << [user, :master]
+ end
+
+ it 'has details' do
+ expect(subject).to have_details
+ end
+ end
+
+ context 'when user does not have permission to read pipeline' do
+ it 'does not have details' do
+ expect(subject).not_to have_details
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
new file mode 100644
index 00000000000..6f8721d30c2
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Stage::Factory do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ let(:stage) do
+ build(:ci_stage, pipeline: pipeline, name: 'test')
+ end
+
+ subject do
+ described_class.new(stage, user)
+ end
+
+ let(:status) do
+ subject.fabricate!
+ end
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'when stage has a core status' do
+ HasStatus::AVAILABLE_STATUSES.each do |core_status|
+ context "when core status is #{core_status}" do
+ before do
+ create(:ci_build, pipeline: pipeline, stage: 'test', status: core_status)
+ create(:commit_status, pipeline: pipeline, stage: 'test', status: core_status)
+ create(:ci_build, pipeline: pipeline, stage: 'build', status: :failed)
+ end
+
+ it "fabricates a core status #{core_status}" do
+ expect(status).to be_a(
+ Gitlab::Ci::Status.const_get(core_status.capitalize))
+ end
+
+ it 'extends core status with common stage methods' do
+ expect(status).to have_details
+ expect(status.details_path).to include "pipelines/#{pipeline.id}"
+ expect(status.details_path).to include "##{stage.name}"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb
new file mode 100644
index 00000000000..90f9f615e0d
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/success_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Success do
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
+
+ describe '#text' do
+ it { expect(subject.label).to eq 'passed' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'passed' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'icon_status_success' }
+ end
+end
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index de3f64249a2..1bbaca0739a 100644
--- a/spec/lib/gitlab/closing_issue_extractor_spec.rb
+++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb
@@ -257,8 +257,9 @@ describe Gitlab::ClosingIssueExtractor, lib: true do
context 'with an external issue tracker reference' do
it 'extracts the referenced issue' do
jira_project = create(:jira_project, name: 'JIRA_EXT1')
+ jira_project.team << [jira_project.creator, :master]
jira_issue = ExternalIssue.new("#{jira_project.name}-1", project: jira_project)
- closing_issue_extractor = described_class.new jira_project
+ closing_issue_extractor = described_class.new(jira_project, jira_project.creator)
message = "Resolve #{jira_issue.to_reference}"
expect(closing_issue_extractor.closed_by_message(message)).to eq([jira_issue])
diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb
new file mode 100644
index 00000000000..01b2a55b63c
--- /dev/null
+++ b/spec/lib/gitlab/contributions_calendar_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe Gitlab::ContributionsCalendar do
+ let(:contributor) { create(:user) }
+ let(:user) { create(:user) }
+
+ let(:private_project) do
+ create(:empty_project, :private) do |project|
+ create(:project_member, user: contributor, project: project)
+ end
+ end
+
+ let(:public_project) do
+ create(:empty_project, :public) do |project|
+ create(:project_member, user: contributor, project: project)
+ end
+ end
+
+ let(:feature_project) do
+ create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) do |project|
+ create(:project_member, user: contributor, project: project).project
+ end
+ end
+
+ let(:today) { Time.now.to_date }
+ let(:last_week) { today - 7.days }
+ let(:last_year) { today - 1.year }
+
+ before do
+ travel_to today
+ end
+
+ after do
+ travel_back
+ end
+
+ def calendar(current_user = nil)
+ described_class.new(contributor, current_user)
+ end
+
+ def create_event(project, day)
+ @targets ||= {}
+ @targets[project] ||= create(:issue, project: project, author: contributor)
+
+ Event.create!(
+ project: project,
+ action: Event::CREATED,
+ target: @targets[project],
+ author: contributor,
+ created_at: day,
+ )
+ end
+
+ describe '#activity_dates' do
+ it "returns a hash of date => count" do
+ create_event(public_project, last_week)
+ create_event(public_project, last_week)
+ create_event(public_project, today)
+
+ expect(calendar.activity_dates).to eq(last_week => 2, today => 1)
+ end
+
+ it "only shows private events to authorized users" do
+ create_event(private_project, today)
+ create_event(feature_project, today)
+
+ expect(calendar.activity_dates[today]).to eq(0)
+ expect(calendar(user).activity_dates[today]).to eq(0)
+ expect(calendar(contributor).activity_dates[today]).to eq(2)
+ end
+ end
+
+ describe '#events_by_date' do
+ it "returns all events for a given date" do
+ e1 = create_event(public_project, today)
+ e2 = create_event(public_project, today)
+ create_event(public_project, last_week)
+
+ expect(calendar.events_by_date(today)).to contain_exactly(e1, e2)
+ end
+
+ it "only shows private events to authorized users" do
+ e1 = create_event(public_project, today)
+ e2 = create_event(private_project, today)
+ e3 = create_event(feature_project, today)
+ create_event(public_project, last_week)
+
+ expect(calendar.events_by_date(today)).to contain_exactly(e1)
+ expect(calendar(contributor).events_by_date(today)).to contain_exactly(e1, e2, e3)
+ end
+ end
+
+ describe '#starting_year' do
+ it "should be the start of last year" do
+ expect(calendar.starting_year).to eq(last_year.year)
+ end
+ end
+
+ describe '#starting_month' do
+ it "should be the start of this month" do
+ expect(calendar.starting_month).to eq(today.month)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/code_event_spec.rb b/spec/lib/gitlab/cycle_analytics/code_event_spec.rb
new file mode 100644
index 00000000000..43f42d1bde8
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/code_event_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::CodeEvent do
+ it_behaves_like 'default query config' do
+ it 'does not have the default order' do
+ expect(event.order).not_to eq(event.start_time_attrs)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb
new file mode 100644
index 00000000000..6062e7af4f5
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb
@@ -0,0 +1,326 @@
+require 'spec_helper'
+
+describe Gitlab::CycleAnalytics::Events do
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
+
+ subject { described_class.new(project: project, options: { from: from_date, current_user: user }) }
+
+ before do
+ allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([context])
+
+ setup(context)
+ end
+
+ describe '#issue_events' do
+ it 'has the total time' do
+ expect(subject.issue_events.first[:total_time]).not_to be_empty
+ end
+
+ it 'has a title' do
+ expect(subject.issue_events.first[:title]).to eq(context.title)
+ end
+
+ it 'has the URL' do
+ expect(subject.issue_events.first[:url]).not_to be_nil
+ end
+
+ it 'has an iid' do
+ expect(subject.issue_events.first[:iid]).to eq(context.iid.to_s)
+ end
+
+ it 'has a created_at timestamp' do
+ expect(subject.issue_events.first[:created_at]).to end_with('ago')
+ end
+
+ it "has the author's URL" do
+ expect(subject.issue_events.first[:author][:web_url]).not_to be_nil
+ end
+
+ it "has the author's avatar URL" do
+ expect(subject.issue_events.first[:author][:avatar_url]).not_to be_nil
+ end
+
+ it "has the author's name" do
+ expect(subject.issue_events.first[:author][:name]).to eq(context.author.name)
+ end
+ end
+
+ describe '#plan_events' do
+ it 'has a title' do
+ expect(subject.plan_events.first[:title]).not_to be_nil
+ end
+
+ it 'has a sha short ID' do
+ expect(subject.plan_events.first[:short_sha]).not_to be_nil
+ end
+
+ it 'has the URL' do
+ expect(subject.plan_events.first[:commit_url]).not_to be_nil
+ end
+
+ it 'has the total time' do
+ expect(subject.plan_events.first[:total_time]).not_to be_empty
+ end
+
+ it "has the author's URL" do
+ expect(subject.plan_events.first[:author][:web_url]).not_to be_nil
+ end
+
+ it "has the author's avatar URL" do
+ expect(subject.plan_events.first[:author][:avatar_url]).not_to be_nil
+ end
+
+ it "has the author's name" do
+ expect(subject.plan_events.first[:author][:name]).not_to be_nil
+ end
+ end
+
+ describe '#code_events' do
+ before do
+ create_commit_referencing_issue(context)
+ end
+
+ it 'has the total time' do
+ expect(subject.code_events.first[:total_time]).not_to be_empty
+ end
+
+ it 'has a title' do
+ expect(subject.code_events.first[:title]).to eq('Awesome merge_request')
+ end
+
+ it 'has an iid' do
+ expect(subject.code_events.first[:iid]).to eq(context.iid.to_s)
+ end
+
+ it 'has a created_at timestamp' do
+ expect(subject.code_events.first[:created_at]).to end_with('ago')
+ end
+
+ it "has the author's URL" do
+ expect(subject.code_events.first[:author][:web_url]).not_to be_nil
+ end
+
+ it "has the author's avatar URL" do
+ expect(subject.code_events.first[:author][:avatar_url]).not_to be_nil
+ end
+
+ it "has the author's name" do
+ expect(subject.code_events.first[:author][:name]).to eq(MergeRequest.first.author.name)
+ end
+ end
+
+ describe '#test_events' do
+ let(:merge_request) { MergeRequest.first }
+ let!(:pipeline) do
+ create(:ci_pipeline,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha,
+ project: context.project)
+ end
+
+ before do
+ create(:ci_build, pipeline: pipeline, status: :success, author: user)
+ create(:ci_build, pipeline: pipeline, status: :success, author: user)
+
+ pipeline.run!
+ pipeline.succeed!
+ end
+
+ it 'has the name' do
+ expect(subject.test_events.first[:name]).not_to be_nil
+ end
+
+ it 'has the ID' do
+ expect(subject.test_events.first[:id]).not_to be_nil
+ end
+
+ it 'has the URL' do
+ expect(subject.test_events.first[:url]).not_to be_nil
+ end
+
+ it 'has the branch name' do
+ expect(subject.test_events.first[:branch]).not_to be_nil
+ end
+
+ it 'has the branch URL' do
+ expect(subject.test_events.first[:branch][:url]).not_to be_nil
+ end
+
+ it 'has the short SHA' do
+ expect(subject.test_events.first[:short_sha]).not_to be_nil
+ end
+
+ it 'has the commit URL' do
+ expect(subject.test_events.first[:commit_url]).not_to be_nil
+ end
+
+ it 'has the date' do
+ expect(subject.test_events.first[:date]).not_to be_nil
+ end
+
+ it 'has the total time' do
+ expect(subject.test_events.first[:total_time]).not_to be_empty
+ end
+ end
+
+ describe '#review_events' do
+ let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
+
+ it 'has the total time' do
+ expect(subject.review_events.first[:total_time]).not_to be_empty
+ end
+
+ it 'has a title' do
+ expect(subject.review_events.first[:title]).to eq('Awesome merge_request')
+ end
+
+ it 'has an iid' do
+ expect(subject.review_events.first[:iid]).to eq(context.iid.to_s)
+ end
+
+ it 'has the URL' do
+ expect(subject.review_events.first[:url]).not_to be_nil
+ end
+
+ it 'has a state' do
+ expect(subject.review_events.first[:state]).not_to be_nil
+ end
+
+ it 'has a created_at timestamp' do
+ expect(subject.review_events.first[:created_at]).not_to be_nil
+ end
+
+ it "has the author's URL" do
+ expect(subject.review_events.first[:author][:web_url]).not_to be_nil
+ end
+
+ it "has the author's avatar URL" do
+ expect(subject.review_events.first[:author][:avatar_url]).not_to be_nil
+ end
+
+ it "has the author's name" do
+ expect(subject.review_events.first[:author][:name]).to eq(MergeRequest.first.author.name)
+ end
+ end
+
+ describe '#staging_events' do
+ let(:merge_request) { MergeRequest.first }
+ let!(:pipeline) do
+ create(:ci_pipeline,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha,
+ project: context.project)
+ end
+
+ before do
+ create(:ci_build, pipeline: pipeline, status: :success, author: user)
+ create(:ci_build, pipeline: pipeline, status: :success, author: user)
+
+ pipeline.run!
+ pipeline.succeed!
+
+ merge_merge_requests_closing_issue(context)
+ deploy_master
+ end
+
+ it 'has the name' do
+ expect(subject.staging_events.first[:name]).not_to be_nil
+ end
+
+ it 'has the ID' do
+ expect(subject.staging_events.first[:id]).not_to be_nil
+ end
+
+ it 'has the URL' do
+ expect(subject.staging_events.first[:url]).not_to be_nil
+ end
+
+ it 'has the branch name' do
+ expect(subject.staging_events.first[:branch]).not_to be_nil
+ end
+
+ it 'has the branch URL' do
+ expect(subject.staging_events.first[:branch][:url]).not_to be_nil
+ end
+
+ it 'has the short SHA' do
+ expect(subject.staging_events.first[:short_sha]).not_to be_nil
+ end
+
+ it 'has the commit URL' do
+ expect(subject.staging_events.first[:commit_url]).not_to be_nil
+ end
+
+ it 'has the date' do
+ expect(subject.staging_events.first[:date]).not_to be_nil
+ end
+
+ it 'has the total time' do
+ expect(subject.staging_events.first[:total_time]).not_to be_empty
+ end
+
+ it "has the author's URL" do
+ expect(subject.staging_events.first[:author][:web_url]).not_to be_nil
+ end
+
+ it "has the author's avatar URL" do
+ expect(subject.staging_events.first[:author][:avatar_url]).not_to be_nil
+ end
+
+ it "has the author's name" do
+ expect(subject.staging_events.first[:author][:name]).to eq(MergeRequest.first.author.name)
+ end
+ end
+
+ describe '#production_events' do
+ let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
+
+ before do
+ merge_merge_requests_closing_issue(context)
+ deploy_master
+ end
+
+ it 'has the total time' do
+ expect(subject.production_events.first[:total_time]).not_to be_empty
+ end
+
+ it 'has a title' do
+ expect(subject.production_events.first[:title]).to eq(context.title)
+ end
+
+ it 'has the URL' do
+ expect(subject.production_events.first[:url]).not_to be_nil
+ end
+
+ it 'has an iid' do
+ expect(subject.production_events.first[:iid]).to eq(context.iid.to_s)
+ end
+
+ it 'has a created_at timestamp' do
+ expect(subject.production_events.first[:created_at]).to end_with('ago')
+ end
+
+ it "has the author's URL" do
+ expect(subject.production_events.first[:author][:web_url]).not_to be_nil
+ end
+
+ it "has the author's avatar URL" do
+ expect(subject.production_events.first[:author][:avatar_url]).not_to be_nil
+ end
+
+ it "has the author's name" do
+ expect(subject.production_events.first[:author][:name]).to eq(context.author.name)
+ end
+ end
+
+ def setup(context)
+ milestone = create(:milestone, project: project)
+ context.update(milestone: milestone)
+ mr = create_merge_request_closing_issue(context)
+
+ ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb
new file mode 100644
index 00000000000..1c5c308da7d
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::IssueEvent do
+ it_behaves_like 'default query config' do
+ it 'has the default order' do
+ expect(event.order).to eq(event.start_time_attrs)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/permissions_spec.rb b/spec/lib/gitlab/cycle_analytics/permissions_spec.rb
new file mode 100644
index 00000000000..2d85e712db0
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/permissions_spec.rb
@@ -0,0 +1,127 @@
+require 'spec_helper'
+
+describe Gitlab::CycleAnalytics::Permissions do
+ let(:project) { create(:empty_project, public_builds: false) }
+ let(:user) { create(:user) }
+
+ subject { described_class.get(user: user, project: project) }
+
+ context 'user with no relation to the project' do
+ it 'has no permissions to issue stage' do
+ expect(subject[:issue]).to eq(false)
+ end
+
+ it 'has no permissions to test stage' do
+ expect(subject[:test]).to eq(false)
+ end
+
+ it 'has no permissions to staging stage' do
+ expect(subject[:staging]).to eq(false)
+ end
+
+ it 'has no permissions to production stage' do
+ expect(subject[:production]).to eq(false)
+ end
+
+ it 'has no permissions to code stage' do
+ expect(subject[:code]).to eq(false)
+ end
+
+ it 'has no permissions to review stage' do
+ expect(subject[:review]).to eq(false)
+ end
+
+ it 'has no permissions to plan stage' do
+ expect(subject[:plan]).to eq(false)
+ end
+ end
+
+ context 'user is master' do
+ before do
+ project.team << [user, :master]
+ end
+
+ it 'has permissions to issue stage' do
+ expect(subject[:issue]).to eq(true)
+ end
+
+ it 'has permissions to test stage' do
+ expect(subject[:test]).to eq(true)
+ end
+
+ it 'has permissions to staging stage' do
+ expect(subject[:staging]).to eq(true)
+ end
+
+ it 'has permissions to production stage' do
+ expect(subject[:production]).to eq(true)
+ end
+
+ it 'has permissions to code stage' do
+ expect(subject[:code]).to eq(true)
+ end
+
+ it 'has permissions to review stage' do
+ expect(subject[:review]).to eq(true)
+ end
+
+ it 'has permissions to plan stage' do
+ expect(subject[:plan]).to eq(true)
+ end
+ end
+
+ context 'user has no build permissions' do
+ before do
+ project.team << [user, :guest]
+ end
+
+ it 'has permissions to issue stage' do
+ expect(subject[:issue]).to eq(true)
+ end
+
+ it 'has no permissions to test stage' do
+ expect(subject[:test]).to eq(false)
+ end
+
+ it 'has no permissions to staging stage' do
+ expect(subject[:staging]).to eq(false)
+ end
+ end
+
+ context 'user has no merge request permissions' do
+ before do
+ project.team << [user, :guest]
+ end
+
+ it 'has permissions to issue stage' do
+ expect(subject[:issue]).to eq(true)
+ end
+
+ it 'has no permissions to code stage' do
+ expect(subject[:code]).to eq(false)
+ end
+
+ it 'has no permissions to review stage' do
+ expect(subject[:review]).to eq(false)
+ end
+ end
+
+ context 'user has no issue permissions' do
+ before do
+ project.team << [user, :developer]
+ project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+ end
+
+ it 'has permissions to code stage' do
+ expect(subject[:code]).to eq(true)
+ end
+
+ it 'has no permissions to issue stage' do
+ expect(subject[:issue]).to eq(false)
+ end
+
+ it 'has no permissions to production stage' do
+ expect(subject[:production]).to eq(false)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb
new file mode 100644
index 00000000000..4a5604115ec
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::PlanEvent do
+ it_behaves_like 'default query config' do
+ it 'has the default order' do
+ expect(event.order).to eq(event.start_time_attrs)
+ end
+
+ context 'no commits' do
+ it 'does not blow up if there are no commits' do
+ allow_any_instance_of(Gitlab::CycleAnalytics::EventsQuery).to receive(:execute).and_return([{}])
+
+ expect { event.fetch }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_spec.rb
new file mode 100644
index 00000000000..ac17e3b4287
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/production_event_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::ProductionEvent do
+ it_behaves_like 'default query config' do
+ it 'has the default order' do
+ expect(event.order).to eq(event.start_time_attrs)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/review_event_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_spec.rb
new file mode 100644
index 00000000000..1ff53aa0227
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/review_event_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::ReviewEvent do
+ it_behaves_like 'default query config' do
+ it 'has the default order' do
+ expect(event.order).to eq(event.start_time_attrs)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb
new file mode 100644
index 00000000000..7019e4c3351
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+shared_examples 'default query config' do
+ let(:event) { described_class.new(project: double, options: {}) }
+
+ it 'has the start attributes' do
+ expect(event.start_time_attrs).not_to be_nil
+ end
+
+ it 'has the stage attribute' do
+ expect(event.stage).not_to be_nil
+ end
+
+ it 'has the end attributes' do
+ expect(event.end_time_attrs).not_to be_nil
+ end
+
+ it 'has the projection attributes' do
+ expect(event.projections).not_to be_nil
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb
new file mode 100644
index 00000000000..4862d4765f2
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::StagingEvent do
+ it_behaves_like 'default query config' do
+ it 'does not have the default order' do
+ expect(event.order).not_to eq(event.start_time_attrs)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/test_event_spec.rb b/spec/lib/gitlab/cycle_analytics/test_event_spec.rb
new file mode 100644
index 00000000000..e249db69fc6
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/test_event_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::TestEvent do
+ it_behaves_like 'default query config' do
+ it 'does not have the default order' do
+ expect(event.order).not_to eq(event.start_time_attrs)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/updater_spec.rb b/spec/lib/gitlab/cycle_analytics/updater_spec.rb
new file mode 100644
index 00000000000..eff54cd3692
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/updater_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Gitlab::CycleAnalytics::Updater do
+ describe 'updates authors' do
+ let(:user) { create(:user) }
+ let(:events) { [{ 'author_id' => user.id }] }
+
+ it 'maps the correct user' do
+ described_class.update!(events, from: 'author_id', to: 'author', klass: User)
+
+ expect(events.first['author']).to eq(user)
+ end
+ end
+
+ describe 'updates builds' do
+ let(:build) { create(:ci_build) }
+ let(:events) { [{ 'id' => build.id }] }
+
+ it 'maps the correct build' do
+ described_class.update!(events, from: 'id', to: 'build', klass: ::Ci::Build)
+
+ expect(events.first['build']).to eq(build)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
new file mode 100644
index 00000000000..2a680f03476
--- /dev/null
+++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Gitlab::Diff::FileCollection::MergeRequestDiff do
+ let(:merge_request) { create :merge_request }
+
+ it 'does not hightlight binary files' do
+ allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(double("text?" => false))
+
+ expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines)
+
+ described_class.new(merge_request.merge_request_diff, diff_options: nil).diff_files
+ end
+
+ it 'does not hightlight file if blob is not accessable' do
+ allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(nil)
+
+ expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines)
+
+ described_class.new(merge_request.merge_request_diff, diff_options: nil).diff_files
+ end
+end
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 0650cb291e5..38475792d93 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -46,4 +46,28 @@ describe Gitlab::Diff::File, lib: true do
expect(diff_file.collapsed?).to eq(false)
end
end
+
+ describe '#old_content_commit' do
+ it 'returns base commit' do
+ old_content_commit = diff_file.old_content_commit
+
+ expect(old_content_commit.id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ end
+ end
+
+ describe '#old_blob' do
+ it 'returns blob of commit of base commit' do
+ old_data = diff_file.old_blob.data
+
+ expect(old_data).to include('raise "System commands must be given as an array of strings"')
+ end
+ end
+
+ describe '#blob' do
+ it 'returns blob of new commit' do
+ data = diff_file.blob.data
+
+ expect(data).to include('raise RuntimeError, "System commands must be given as an array of strings"')
+ end
+ end
end
diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb
index 6f8e9a4be64..c7a0139d32a 100644
--- a/spec/lib/gitlab/email/reply_parser_spec.rb
+++ b/spec/lib/gitlab/email/reply_parser_spec.rb
@@ -206,5 +206,9 @@ describe Gitlab::Email::ReplyParser, lib: true do
it "properly renders email reply from MS Outlook client" do
expect(test_parse_body(fixture_file("emails/outlook.eml"))).to eq("Microsoft Outlook 2010")
end
+
+ it "properly renders html-only email from MS Outlook" do
+ expect(test_parse_body(fixture_file("emails/outlook_html.eml"))).to eq("Microsoft Outlook 2010")
+ end
end
end
diff --git a/spec/lib/gitlab/file_detector_spec.rb b/spec/lib/gitlab/file_detector_spec.rb
new file mode 100644
index 00000000000..e5ba13bbaf8
--- /dev/null
+++ b/spec/lib/gitlab/file_detector_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe Gitlab::FileDetector do
+ describe '.types_in_paths' do
+ it 'returns the file types for the given paths' do
+ expect(described_class.types_in_paths(%w(README.md CHANGELOG VERSION VERSION))).
+ to eq(%i{readme changelog version})
+ end
+
+ it 'does not include unrecognized file paths' do
+ expect(described_class.types_in_paths(%w(README.md foo.txt))).
+ to eq(%i{readme})
+ end
+ end
+
+ describe '.type_of' do
+ it 'returns the type of a README file' do
+ expect(described_class.type_of('README.md')).to eq(:readme)
+ end
+
+ it 'returns the type of a changelog file' do
+ %w(CHANGELOG HISTORY CHANGES NEWS).each do |file|
+ expect(described_class.type_of(file)).to eq(:changelog)
+ end
+ end
+
+ it 'returns the type of a license file' do
+ %w(LICENSE LICENCE COPYING).each do |file|
+ expect(described_class.type_of(file)).to eq(:license)
+ end
+ end
+
+ it 'returns the type of a version file' do
+ expect(described_class.type_of('VERSION')).to eq(:version)
+ end
+
+ it 'returns the type of a .gitignore file' do
+ expect(described_class.type_of('.gitignore')).to eq(:gitignore)
+ end
+
+ it 'returns the type of a Koding config file' do
+ expect(described_class.type_of('.koding.yml')).to eq(:koding)
+ end
+
+ it 'returns the type of a GitLab CI config file' do
+ expect(described_class.type_of('.gitlab-ci.yml')).to eq(:gitlab_ci)
+ end
+
+ it 'returns the type of an avatar' do
+ %w(logo.gif logo.png logo.jpg).each do |file|
+ expect(described_class.type_of(file)).to eq(:avatar)
+ end
+ end
+
+ it 'returns nil for an unknown file' do
+ expect(described_class.type_of('foo.txt')).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
index f045463c1cb..f4703dc704f 100644
--- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Gfm::ReferenceRewriter do
let(:new_project) { create(:project, name: 'new') }
let(:user) { create(:user) }
- before { old_project.team << [user, :guest] }
+ before { old_project.team << [user, :reporter] }
describe '#rewrite' do
subject do
@@ -29,7 +29,7 @@ describe Gitlab::Gfm::ReferenceRewriter do
context 'description with ignored elements' do
let(:text) do
"Hi. This references #1, but not `#2`\n" +
- '<pre>and not !1</pre>'
+ '<pre>and not !1</pre>'
end
it { is_expected.to include issue_first.to_reference(new_project) }
@@ -64,7 +64,7 @@ describe Gitlab::Gfm::ReferenceRewriter do
context 'description with project labels' do
let!(:label) { create(:label, id: 123, name: 'test', project: old_project) }
- let(:project_ref) { old_project.to_reference }
+ let(:project_ref) { old_project.to_reference(new_project) }
context 'label referenced by id' do
let(:text) { '#1 and ~123' }
@@ -80,7 +80,7 @@ describe Gitlab::Gfm::ReferenceRewriter do
context 'description with group labels' do
let(:old_group) { create(:group) }
let!(:group_label) { create(:group_label, id: 321, name: 'group label', group: old_group) }
- let(:project_ref) { old_project.to_reference }
+ let(:project_ref) { old_project.to_reference(new_project) }
before do
old_project.update(namespace: old_group)
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 62aa212f1f6..f1d0a190002 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -66,6 +66,7 @@ describe Gitlab::GitAccess, lib: true do
context 'pull code' do
it { expect(subject.allowed?).to be_falsey }
+ it { expect(subject.message).to match(/You are not allowed to download code/) }
end
end
@@ -77,6 +78,7 @@ describe Gitlab::GitAccess, lib: true do
context 'pull code' do
it { expect(subject.allowed?).to be_falsey }
+ it { expect(subject.message).to match(/Your account has been blocked/) }
end
end
@@ -84,6 +86,29 @@ describe Gitlab::GitAccess, lib: true do
context 'pull code' do
it { expect(subject.allowed?).to be_falsey }
end
+
+ context 'when project is public' do
+ let(:public_project) { create(:project, :public) }
+ let(:guest_access) { Gitlab::GitAccess.new(nil, public_project, 'web', authentication_abilities: []) }
+ subject { guest_access.check('git-upload-pack', '_any') }
+
+ context 'when repository is enabled' do
+ it 'give access to download code' do
+ public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::ENABLED)
+
+ expect(subject.allowed?).to be_truthy
+ end
+ end
+
+ context 'when repository is disabled' do
+ it 'does not give access to download code' do
+ public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
+
+ expect(subject.allowed?).to be_falsey
+ expect(subject.message).to match(/You are not allowed to download code/)
+ end
+ end
+ end
end
describe 'deploy key permissions' do
diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb
index 576cda595bb..578db51631e 100644
--- a/spec/lib/gitlab/git_access_wiki_spec.rb
+++ b/spec/lib/gitlab/git_access_wiki_spec.rb
@@ -18,7 +18,7 @@ describe Gitlab::GitAccessWiki, lib: true do
project.team << [user, :developer]
end
- subject { access.push_access_check(changes) }
+ subject { access.check('git-receive-pack', changes) }
it { expect(subject.allowed?).to be_truthy }
end
@@ -26,4 +26,29 @@ describe Gitlab::GitAccessWiki, lib: true do
def changes
['6f6d7e7ed 570e7b2ab refs/heads/master']
end
+
+ describe '#download_access_check' do
+ subject { access.check('git-upload-pack', '_any') }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ 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
+
+ context 'when wiki feature is disabled' do
+ it 'does not give access to download wiki code' do
+ project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
+
+ expect(subject.allowed?).to be_falsey
+ expect(subject.message).to match(/You are not allowed to download code/)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb
index e5300dbba1e..462caa5b5fe 100644
--- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb
@@ -49,14 +49,20 @@ describe Gitlab::GithubImport::BranchFormatter, lib: true do
end
describe '#valid?' do
- it 'returns true when raw repo is present' do
+ it 'returns true when raw sha and ref are present' do
branch = described_class.new(project, double(raw))
expect(branch.valid?).to eq true
end
- it 'returns false when raw repo is blank' do
- branch = described_class.new(project, double(raw.merge(repo: nil)))
+ it 'returns false when raw sha is blank' do
+ branch = described_class.new(project, double(raw.merge(sha: nil)))
+
+ expect(branch.valid?).to eq false
+ end
+
+ it 'returns false when raw ref is blank' do
+ branch = described_class.new(project, double(raw.merge(ref: nil)))
expect(branch.valid?).to eq false
end
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
index 7478f86bd28..9e027839f59 100644
--- a/spec/lib/gitlab/github_import/importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -101,7 +101,6 @@ describe Gitlab::GithubImport::Importer, lib: true do
closed_at: nil,
merged_at: nil,
url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347',
- labels: [double(name: 'Label #3')],
)
end
@@ -156,9 +155,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
message: 'The remote data could not be fully imported.',
errors: [
{ type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" },
- { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank, Title is too short (minimum is 0 characters)" },
- { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." },
- { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." },
+ { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank" },
{ type: :wiki, errors: "Gitlab::Shell::Error" },
{ type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" }
]
diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
index c2f1f6b91a1..95339e2f128 100644
--- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
@@ -144,20 +144,20 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
end
end
- describe '#valid?' do
+ describe '#pull_request?' do
context 'when mention a pull request' do
let(:raw_data) { double(base_data.merge(pull_request: double)) }
- it 'returns false' do
- expect(issue.valid?).to eq false
+ it 'returns true' do
+ expect(issue.pull_request?).to eq true
end
end
context 'when does not mention a pull request' do
let(:raw_data) { double(base_data.merge(pull_request: nil)) }
- it 'returns true' do
- expect(issue.valid?).to eq true
+ it 'returns false' do
+ expect(issue.pull_request?).to eq false
end
end
end
diff --git a/spec/lib/gitlab/identifier_spec.rb b/spec/lib/gitlab/identifier_spec.rb
index 47d6f1007d1..bb758a8a202 100644
--- a/spec/lib/gitlab/identifier_spec.rb
+++ b/spec/lib/gitlab/identifier_spec.rb
@@ -40,7 +40,7 @@ describe Gitlab::Identifier do
describe '#identify_using_commit' do
it "returns the User for an existing commit author's Email address" do
- commit = double(:commit, author_email: user.email)
+ commit = double(:commit, author: user, author_email: user.email)
expect(project).to receive(:commit).with('123').and_return(commit)
@@ -62,10 +62,9 @@ describe Gitlab::Identifier do
end
it 'caches the found users per Email' do
- commit = double(:commit, author_email: user.email)
+ commit = double(:commit, author: user, author_email: user.email)
expect(project).to receive(:commit).with('123').twice.and_return(commit)
- expect(User).to receive(:find_by).once.and_call_original
2.times do
expect(identifier.identify_using_commit(project, '123')).to eq(user)
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 02b11bd999a..9b49d6837c3 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -116,6 +116,7 @@ project:
- base_tags
- tag_taggings
- tags
+- chat_services
- creator
- group
- namespace
@@ -127,6 +128,7 @@ project:
- emails_on_push_service
- builds_email_service
- pipelines_email_service
+- mattermost_slash_commands_service
- irker_service
- pivotaltracker_service
- hipchat_service
@@ -134,7 +136,8 @@ project:
- assembla_service
- asana_service
- gemnasium_service
-- slack_service
+- slack_notification_service
+- mattermost_notification_service
- buildkite_service
- bamboo_service
- teamcity_service
@@ -145,6 +148,7 @@ project:
- bugzilla_service
- gitlab_issue_tracker_service
- external_wiki_service
+- kubernetes_service
- forked_project_link
- forked_from_project
- forked_project_links
@@ -184,8 +188,11 @@ project:
- environments
- deployments
- project_feature
+- authorized_users
+- project_authorizations
+- route
award_emoji:
- awardable
- user
priorities:
-- label \ No newline at end of file
+- label
diff --git a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb
index 5ae178414cc..08a42fd27a2 100644
--- a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb
@@ -1,12 +1,14 @@
require 'spec_helper'
describe Gitlab::ImportExport::AvatarRestorer, lib: true do
+ include UploadHelpers
+
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') }
let(:project) { create(:empty_project) }
before do
allow_any_instance_of(described_class).to receive(:avatar_export_file)
- .and_return(Rails.root + "spec/fixtures/dk.png")
+ .and_return(uploaded_image_temp_path)
end
after do
diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb
index 770e8b0c2f4..1cb02f8e318 100644
--- a/spec/lib/gitlab/import_export/members_mapper_spec.rb
+++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb
@@ -2,9 +2,9 @@ require 'spec_helper'
describe Gitlab::ImportExport::MembersMapper, services: true do
describe 'map members' do
- let(:user) { create(:user) }
+ let(:user) { create(:user, authorized_projects_populated: true) }
let(:project) { create(:project, :public, name: 'searchable_project') }
- let(:user2) { create(:user) }
+ let(:user2) { create(:user, authorized_projects_populated: true) }
let(:exported_user_id) { 99 }
let(:exported_members) do
[{
@@ -67,5 +67,12 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
expect(ProjectMember.find_by_invite_email('invite@test.com')).not_to be_nil
end
+
+ it 'authorizes the users to the project' do
+ members_mapper.map
+
+ expect(user.authorized_project?(project)).to be true
+ expect(user2.authorized_project?(project)).to be true
+ 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 07a2c316899..78d6b2c5032 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -78,6 +78,7 @@ Milestone:
- project_id
- description
- due_date
+- start_date
- created_at
- updated_at
- state
@@ -258,6 +259,7 @@ Service:
- template
- push_events
- issues_events
+- commit_events
- merge_requests_events
- tag_push_events
- note_events
@@ -339,4 +341,4 @@ LabelPriority:
- label_id
- priority
- created_at
-- updated_at \ No newline at end of file
+- updated_at
diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb
index 835853a83a4..1a6803e01c3 100644
--- a/spec/lib/gitlab/ldap/config_spec.rb
+++ b/spec/lib/gitlab/ldap/config_spec.rb
@@ -1,20 +1,132 @@
require 'spec_helper'
describe Gitlab::LDAP::Config, lib: true do
- let(:config) { Gitlab::LDAP::Config.new provider }
- let(:provider) { 'ldapmain' }
+ include LdapHelpers
+
+ let(:config) { Gitlab::LDAP::Config.new('ldapmain') }
describe '#initalize' do
it 'requires a provider' do
expect{ Gitlab::LDAP::Config.new }.to raise_error ArgumentError
end
- it "works" do
+ it 'works' do
expect(config).to be_a described_class
end
- it "raises an error if a unknow provider is used" do
+ it 'raises an error if a unknown provider is used' do
expect{ Gitlab::LDAP::Config.new 'unknown' }.to raise_error(RuntimeError)
end
end
+
+ describe '#adapter_options' do
+ it 'constructs basic options' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 386,
+ 'method' => 'plain'
+ }
+ )
+
+ expect(config.adapter_options).to eq(
+ host: 'ldap.example.com',
+ port: 386,
+ encryption: nil
+ )
+ end
+
+ it 'includes authentication options when auth is configured' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'method' => 'ssl',
+ 'bind_dn' => 'uid=admin,dc=example,dc=com',
+ 'password' => 'super_secret'
+ }
+ )
+
+ expect(config.adapter_options).to eq(
+ host: 'ldap.example.com',
+ port: 686,
+ encryption: :simple_tls,
+ auth: {
+ method: :simple,
+ username: 'uid=admin,dc=example,dc=com',
+ password: 'super_secret'
+ }
+ )
+ end
+ end
+
+ describe '#omniauth_options' do
+ it 'constructs basic options' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 386,
+ 'base' => 'ou=users,dc=example,dc=com',
+ 'method' => 'plain',
+ 'uid' => 'uid'
+ }
+ )
+
+ expect(config.omniauth_options).to include(
+ host: 'ldap.example.com',
+ port: 386,
+ base: 'ou=users,dc=example,dc=com',
+ method: 'plain',
+ filter: '(uid=%{username})'
+ )
+ expect(config.omniauth_options.keys).not_to include(:bind_dn, :password)
+ end
+
+ it 'includes authentication options when auth is configured' do
+ stub_ldap_config(
+ options: {
+ 'uid' => 'sAMAccountName',
+ 'user_filter' => '(memberOf=cn=group1,ou=groups,dc=example,dc=com)',
+ 'bind_dn' => 'uid=admin,dc=example,dc=com',
+ 'password' => 'super_secret'
+ }
+ )
+
+ expect(config.omniauth_options).to include(
+ filter: '(&(sAMAccountName=%{username})(memberOf=cn=group1,ou=groups,dc=example,dc=com))',
+ bind_dn: 'uid=admin,dc=example,dc=com',
+ password: 'super_secret'
+ )
+ end
+ end
+
+ describe '#has_auth?' do
+ it 'is true when password is set' do
+ stub_ldap_config(
+ options: {
+ 'bind_dn' => 'uid=admin,dc=example,dc=com',
+ 'password' => 'super_secret'
+ }
+ )
+
+ expect(config.has_auth?).to be_truthy
+ end
+
+ it 'is true when bind_dn is set and password is empty' do
+ stub_ldap_config(
+ options: {
+ 'bind_dn' => 'uid=admin,dc=example,dc=com',
+ 'password' => ''
+ }
+ )
+
+ expect(config.has_auth?).to be_truthy
+ end
+
+ it 'is false when password and bind_dn are not set' do
+ stub_ldap_config(options: { 'bind_dn' => nil, 'password' => nil })
+
+ expect(config.has_auth?).to be_falsey
+ end
+ end
end
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
index 117a15264da..fd3769d75b5 100644
--- a/spec/lib/gitlab/middleware/go_spec.rb
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -22,7 +22,7 @@ describe Gitlab::Middleware::Go, lib: true do
resp = middleware.call(env)
expect(resp[0]).to eq(200)
expect(resp[1]['Content-Type']).to eq('text/html')
- expected_body = "<!DOCTYPE html><html><head><meta content='localhost/group/project git http://localhost/group/project.git' name='go-import'></head></html>\n"
+ expected_body = "<!DOCTYPE html><html><head><meta content='#{Gitlab.config.gitlab.host}/group/project git http://#{Gitlab.config.gitlab.host}/group/project.git' name='go-import'></head></html>\n"
expect(resp[2].body).to eq([expected_body])
end
end
diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb
new file mode 100644
index 00000000000..ab1ab22795c
--- /dev/null
+++ b/spec/lib/gitlab/middleware/multipart_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+require 'tempfile'
+
+describe Gitlab::Middleware::Multipart do
+ let(:app) { double(:app) }
+ let(:middleware) { described_class.new(app) }
+
+ it 'opens top-level files' do
+ Tempfile.open('top-level') do |tempfile|
+ env = post_env({ 'file' => tempfile.path }, { 'file.name' => 'filename' }, Gitlab::Workhorse.secret, 'gitlab-workhorse')
+
+ expect(app).to receive(:call) do |env|
+ file = Rack::Request.new(env).params['file']
+ expect(file).to be_a(File)
+ expect(file.path).to eq(tempfile.path)
+ end
+
+ middleware.call(env)
+ end
+ end
+
+ it 'rejects headers signed with the wrong secret' do
+ env = post_env({ 'file' => '/var/empty/nonesuch' }, {}, 'x' * 32, 'gitlab-workhorse')
+
+ expect { middleware.call(env) }.to raise_error(JWT::VerificationError)
+ end
+
+ it 'rejects headers signed with the wrong issuer' do
+ env = post_env({ 'file' => '/var/empty/nonesuch' }, {}, Gitlab::Workhorse.secret, 'acme-inc')
+
+ expect { middleware.call(env) }.to raise_error(JWT::InvalidIssuerError)
+ end
+
+ it 'opens files one level deep' do
+ Tempfile.open('one-level') do |tempfile|
+ in_params = { 'user' => { 'avatar' => { '.name' => 'filename' } } }
+ env = post_env({ 'user[avatar]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse')
+
+ expect(app).to receive(:call) do |env|
+ file = Rack::Request.new(env).params['user']['avatar']
+ expect(file).to be_a(File)
+ expect(file.path).to eq(tempfile.path)
+ end
+
+ middleware.call(env)
+ end
+ end
+
+ it 'opens files two levels deep' do
+ Tempfile.open('two-levels') do |tempfile|
+ in_params = { 'project' => { 'milestone' => { 'themesong' => { '.name' => 'filename' } } } }
+ env = post_env({ 'project[milestone][themesong]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse')
+
+ expect(app).to receive(:call) do |env|
+ file = Rack::Request.new(env).params['project']['milestone']['themesong']
+ expect(file).to be_a(File)
+ expect(file.path).to eq(tempfile.path)
+ end
+
+ middleware.call(env)
+ end
+ end
+
+ def post_env(rewritten_fields, params, secret, issuer)
+ token = JWT.encode({ 'iss' => issuer, 'rewritten_fields' => rewritten_fields }, secret, 'HS256')
+ Rack::MockRequest.env_for(
+ '/',
+ method: 'post',
+ params: params,
+ described_class::RACK_ENV_KEY => token
+ )
+ end
+end
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index 78c669e8fa5..fc9e1cb430a 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -137,11 +137,12 @@ describe Gitlab::OAuth::User, lib: true do
allow(ldap_user).to receive(:username) { uid }
allow(ldap_user).to receive(:email) { ['johndoe@example.com', 'john2@example.com'] }
allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' }
- allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
end
context "and no account for the LDAP user" do
it "creates a user with dual LDAP and omniauth identities" do
+ allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
+
oauth_user.save
expect(gl_user).to be_valid
@@ -159,6 +160,8 @@ describe Gitlab::OAuth::User, lib: true do
context "and LDAP user has an account already" do
let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') }
it "adds the omniauth identity to the LDAP account" do
+ allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
+
oauth_user.save
expect(gl_user).to be_valid
@@ -172,6 +175,24 @@ describe Gitlab::OAuth::User, lib: true do
])
end
end
+
+ context 'when an LDAP person is not found by uid' do
+ it 'tries to find an LDAP person by DN and adds the omniauth identity to the user' do
+ allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil)
+ allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user)
+
+ oauth_user.save
+
+ identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
+ expect(identities_as_hash)
+ .to match_array(
+ [
+ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ { provider: 'twitter', extern_uid: uid }
+ ]
+ )
+ end
+ end
end
context "and no corresponding LDAP person" do
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 29abb4d4d07..14ee386dba6 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -6,22 +6,73 @@ describe Gitlab::ProjectSearchResults, lib: true do
let(:query) { 'hello world' }
describe 'initialize with empty ref' do
- let(:results) { Gitlab::ProjectSearchResults.new(user, project, query, '') }
+ let(:results) { described_class.new(user, project, query, '') }
it { expect(results.project).to eq(project) }
- it { expect(results.repository_ref).to be_nil }
it { expect(results.query).to eq('hello world') }
end
describe 'initialize with ref' do
let(:ref) { 'refs/heads/test' }
- let(:results) { Gitlab::ProjectSearchResults.new(user, project, query, ref) }
+ let(:results) { described_class.new(user, project, query, ref) }
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to eq(ref) }
it { expect(results.query).to eq('hello world') }
end
+ describe 'blob search' do
+ let(:results) { described_class.new(user, project, 'files').objects('blobs') }
+
+ it 'finds by name' do
+ expect(results).to include(["files/images/wm.svg", nil])
+ end
+
+ it 'finds by content' do
+ blob = results.select { |result| result.first == "CHANGELOG" }.flatten.last
+
+ expect(blob.filename).to eq("CHANGELOG")
+ end
+
+ describe 'parsing results' do
+ let(:results) { project.repository.search_files_by_content('feature', 'master') }
+ let(:search_result) { results.first }
+
+ subject { described_class.parse_search_result(search_result) }
+
+ it "returns a valid OpenStruct object" do
+ is_expected.to be_an OpenStruct
+ expect(subject.filename).to eq('CHANGELOG')
+ expect(subject.basename).to eq('CHANGELOG')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(188)
+ expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n")
+ end
+
+ context "when filename has extension" do
+ let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" }
+
+ it { expect(subject.filename).to eq('CONTRIBUTE.md') }
+ it { expect(subject.basename).to eq('CONTRIBUTE') }
+ end
+
+ context "when file under directory" do
+ let(:search_result) { "master:a/b/c.md:5:a b c\n" }
+
+ it { expect(subject.filename).to eq('a/b/c.md') }
+ it { expect(subject.basename).to eq('a/b/c') }
+ end
+ end
+ end
+
+ it 'does not list issues on private projects' do
+ issue = create(:issue, project: project)
+
+ results = described_class.new(user, project, issue.title)
+
+ expect(results.objects('issues')).not_to include issue
+ end
+
describe 'confidential issues' do
let(:query) { 'issue' }
let(:author) { create(:user) }
@@ -29,6 +80,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
let(:non_member) { create(:user) }
let(:member) { create(:user) }
let(:admin) { create(:admin) }
+ let(:project) { create(:empty_project, :internal) }
let!(:issue) { create(:issue, project: project, title: 'Issue 1') }
let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) }
@@ -66,7 +118,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
it 'lists project confidential issues for assignee' do
- results = described_class.new(assignee, project.id, query)
+ results = described_class.new(assignee, project, query)
issues = results.objects('issues')
expect(issues).to include issue
@@ -97,4 +149,33 @@ describe Gitlab::ProjectSearchResults, lib: true do
expect(results.issues_count).to eq 3
end
end
+
+ describe 'notes search' do
+ it 'lists notes' do
+ project = create(:empty_project, :public)
+ note = create(:note, project: project)
+
+ results = described_class.new(user, project, note.note)
+
+ expect(results.objects('notes')).to include note
+ end
+
+ it "doesn't list issue notes when access is restricted" do
+ project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ note = create(:note_on_issue, project: project)
+
+ results = described_class.new(user, project, note.note)
+
+ expect(results.objects('notes')).not_to include note
+ end
+
+ it "doesn't list merge_request notes when access is restricted" do
+ project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE)
+ note = create(:note_on_merge_request, project: project)
+
+ results = described_class.new(user, project, note.note)
+
+ expect(results.objects('notes')).not_to include note
+ end
+ end
end
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 7b4ccc83915..bf0ab9635fd 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -3,6 +3,8 @@ require 'spec_helper'
describe Gitlab::ReferenceExtractor, lib: true do
let(:project) { create(:project) }
+ before { project.team << [project.creator, :developer] }
+
subject { Gitlab::ReferenceExtractor.new(project, project.creator) }
it 'accesses valid user objects' do
@@ -42,7 +44,6 @@ describe Gitlab::ReferenceExtractor, lib: true do
end
it 'accesses valid issue objects' do
- project.team << [project.creator, :developer]
@i0 = create(:issue, project: project)
@i1 = create(:issue, project: project)
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index c51b10bdc69..c78cd30157e 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -29,4 +29,20 @@ describe Gitlab::Regex, lib: true do
describe 'file path regex' do
it { expect('foo@/bar').to match(Gitlab::Regex.file_path_regex) }
end
+
+ describe 'environment slug regex' do
+ def be_matched
+ match(Gitlab::Regex.environment_slug_regex)
+ end
+
+ it { expect('foo').to be_matched }
+ it { expect('foo-1').to be_matched }
+
+ it { expect('FOO').not_to be_matched }
+ it { expect('foo/1').not_to be_matched }
+ it { expect('foo.1').not_to be_matched }
+ it { expect('foo*1').not_to be_matched }
+ it { expect('9foo').not_to be_matched }
+ it { expect('foo-').not_to be_matched }
+ end
end
diff --git a/spec/lib/gitlab/routing_spec.rb b/spec/lib/gitlab/routing_spec.rb
new file mode 100644
index 00000000000..01d5acfc15b
--- /dev/null
+++ b/spec/lib/gitlab/routing_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Gitlab::Routing do
+ context 'when module is included' do
+ subject do
+ Class.new.include(described_class).new
+ end
+
+ it 'makes it possible to access url helpers' do
+ expect(subject).to respond_to(:namespace_project_path)
+ end
+ end
+
+ context 'when module is not included' do
+ subject do
+ Class.new.include(described_class.url_helpers).new
+ end
+
+ it 'exposes url helpers module through a method' do
+ expect(subject).to respond_to(:namespace_project_path)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index dfbefad6367..9614aad3e73 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -12,35 +12,57 @@ describe Gitlab::SearchResults do
let!(:milestone) { create(:milestone, project: project, title: 'foo') }
let(:results) { described_class.new(user, Project.all, 'foo') }
- describe '#projects_count' do
- it 'returns the total amount of projects' do
- expect(results.projects_count).to eq(1)
+ context 'as a user with access' do
+ before do
+ project.team << [user, :developer]
end
- end
- describe '#issues_count' do
- it 'returns the total amount of issues' do
- expect(results.issues_count).to eq(1)
+ describe '#projects_count' do
+ it 'returns the total amount of projects' do
+ expect(results.projects_count).to eq(1)
+ end
end
- end
- describe '#merge_requests_count' do
- it 'returns the total amount of merge requests' do
- expect(results.merge_requests_count).to eq(1)
+ describe '#issues_count' do
+ it 'returns the total amount of issues' do
+ expect(results.issues_count).to eq(1)
+ end
+ end
+
+ describe '#merge_requests_count' do
+ it 'returns the total amount of merge requests' do
+ expect(results.merge_requests_count).to eq(1)
+ end
end
- end
- describe '#milestones_count' do
- it 'returns the total amount of milestones' do
- expect(results.milestones_count).to eq(1)
+ describe '#milestones_count' do
+ it 'returns the total amount of milestones' do
+ expect(results.milestones_count).to eq(1)
+ end
end
+
+ it 'includes merge requests from source and target projects' do
+ forked_project = create(:empty_project, forked_from_project: project)
+ merge_request_2 = create(:merge_request, target_project: project, source_project: forked_project, title: 'foo')
+
+ results = described_class.new(user, Project.where(id: forked_project.id), 'foo')
+
+ expect(results.objects('merge_requests')).to include merge_request_2
+ end
+ end
+
+ it 'does not list issues on private projects' do
+ private_project = create(:empty_project, :private)
+ issue = create(:issue, project: private_project, title: 'foo')
+
+ expect(results.objects('issues')).not_to include issue
end
describe 'confidential issues' do
- let(:project_1) { create(:empty_project) }
- let(:project_2) { create(:empty_project) }
- let(:project_3) { create(:empty_project) }
- let(:project_4) { create(:empty_project) }
+ let(:project_1) { create(:empty_project, :internal) }
+ let(:project_2) { create(:empty_project, :internal) }
+ let(:project_3) { create(:empty_project, :internal) }
+ let(:project_4) { create(:empty_project, :internal) }
let(:query) { 'issue' }
let(:limit_projects) { Project.where(id: [project_1.id, project_2.id, project_3.id]) }
let(:author) { create(:user) }
@@ -139,4 +161,11 @@ describe Gitlab::SearchResults do
expect(results.issues_count).to eq 5
end
end
+
+ it 'does not list merge requests on projects with limited access' do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+
+ expect(results.objects('merge_requests')).not_to include merge_request
+ end
end
diff --git a/spec/lib/gitlab/sidekiq_throttler_spec.rb b/spec/lib/gitlab/sidekiq_throttler_spec.rb
new file mode 100644
index 00000000000..ff32e0e699d
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_throttler_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Gitlab::SidekiqThrottler do
+ before do
+ Sidekiq.options[:concurrency] = 35
+
+ stub_application_setting(
+ sidekiq_throttling_enabled: true,
+ sidekiq_throttling_factor: 0.1,
+ sidekiq_throttling_queues: %w[build project_cache]
+ )
+ end
+
+ describe '#execute!' do
+ it 'sets limits on the selected queues' do
+ Gitlab::SidekiqThrottler.execute!
+
+ expect(Sidekiq::Queue['build'].limit).to eq 4
+ expect(Sidekiq::Queue['project_cache'].limit).to eq 4
+ end
+
+ it 'does not set limits on other queues' do
+ Gitlab::SidekiqThrottler.execute!
+
+ expect(Sidekiq::Queue['merge'].limit).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb
index 0cdbab87544..849edb09476 100644
--- a/spec/lib/gitlab/sql/union_spec.rb
+++ b/spec/lib/gitlab/sql/union_spec.rb
@@ -1,16 +1,26 @@
require 'spec_helper'
describe Gitlab::SQL::Union, lib: true do
+ let(:relation_1) { User.where(email: 'alice@example.com').select(:id) }
+ let(:relation_2) { User.where(email: 'bob@example.com').select(:id) }
+
+ def to_sql(relation)
+ relation.reorder(nil).to_sql
+ end
+
describe '#to_sql' do
it 'returns a String joining relations together using a UNION' do
- rel1 = User.where(email: 'alice@example.com')
- rel2 = User.where(email: 'bob@example.com')
- union = described_class.new([rel1, rel2])
+ union = described_class.new([relation_1, relation_2])
+
+ expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}")
+ end
- sql1 = rel1.reorder(nil).to_sql
- sql2 = rel2.reorder(nil).to_sql
+ it 'skips Model.none segements' do
+ empty_relation = User.none
+ union = described_class.new([empty_relation, relation_1, relation_2])
- expect(union.to_sql).to eq("#{sql1}\nUNION\n#{sql2}")
+ expect{User.where("users.id IN (#{union.to_sql})").to_a}.not_to raise_error
+ expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}")
end
end
end
diff --git a/spec/lib/light_url_builder_spec.rb b/spec/lib/light_url_builder_spec.rb
new file mode 100644
index 00000000000..a826b24419a
--- /dev/null
+++ b/spec/lib/light_url_builder_spec.rb
@@ -0,0 +1,119 @@
+require 'spec_helper'
+
+describe Gitlab::UrlBuilder, lib: true do
+ describe '.build' do
+ context 'when passing a Commit' do
+ it 'returns a proper URL' do
+ commit = build_stubbed(:commit)
+
+ url = described_class.build(commit)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{commit.project.path_with_namespace}/commit/#{commit.id}"
+ end
+ end
+
+ context 'when passing an Issue' do
+ it 'returns a proper URL' do
+ issue = build_stubbed(:issue, iid: 42)
+
+ url = described_class.build(issue)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}"
+ end
+ end
+
+ context 'when passing a MergeRequest' do
+ it 'returns a proper URL' do
+ merge_request = build_stubbed(:merge_request, iid: 42)
+
+ url = described_class.build(merge_request)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}"
+ end
+ end
+
+ context 'when passing a Note' do
+ context 'on a Commit' do
+ it 'returns a proper URL' do
+ note = build_stubbed(:note_on_commit)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}"
+ end
+ end
+
+ context 'on a Commit Diff' do
+ it 'returns a proper URL' do
+ note = build_stubbed(:diff_note_on_commit)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}"
+ end
+ end
+
+ context 'on an Issue' do
+ it 'returns a proper URL' do
+ issue = create(:issue, iid: 42)
+ note = build_stubbed(:note_on_issue, noteable: issue)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}#note_#{note.id}"
+ end
+ end
+
+ context 'on a MergeRequest' do
+ it 'returns a proper URL' do
+ merge_request = create(:merge_request, iid: 42)
+ note = build_stubbed(:note_on_merge_request, noteable: merge_request)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}"
+ end
+ end
+
+ context 'on a MergeRequest Diff' do
+ it 'returns a proper URL' do
+ merge_request = create(:merge_request, iid: 42)
+ note = build_stubbed(:diff_note_on_merge_request, noteable: merge_request)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}"
+ end
+ end
+
+ context 'on a ProjectSnippet' do
+ it 'returns a proper URL' do
+ project_snippet = create(:project_snippet)
+ note = build_stubbed(:note_on_project_snippet, noteable: project_snippet)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{project_snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}"
+ end
+ end
+
+ context 'on another object' do
+ it 'returns a proper URL' do
+ project = build_stubbed(:project)
+
+ expect { described_class.build(project) }.
+ to raise_error(NotImplementedError, 'No URL builder defined for Project')
+ end
+ end
+ end
+
+ context 'when passing a WikiPage' do
+ it 'returns a proper URL' do
+ wiki_page = build(:wiki_page)
+ url = described_class.build(wiki_page)
+
+ expect(url).to eq "#{Gitlab.config.gitlab.url}#{wiki_page.wiki.wiki_base_path}/#{wiki_page.slug}"
+ end
+ end
+ end
+end
diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb
new file mode 100644
index 00000000000..3c2eddbd221
--- /dev/null
+++ b/spec/lib/mattermost/session_spec.rb
@@ -0,0 +1,99 @@
+require 'spec_helper'
+
+describe Mattermost::Session, type: :request do
+ let(:user) { create(:user) }
+
+ let(:gitlab_url) { "http://gitlab.com" }
+ let(:mattermost_url) { "http://mattermost.com" }
+
+ subject { described_class.new(user) }
+
+ # Needed for doorkeeper to function
+ it { is_expected.to respond_to(:current_resource_owner) }
+ it { is_expected.to respond_to(:request) }
+ it { is_expected.to respond_to(:authorization) }
+ it { is_expected.to respond_to(:strategy) }
+
+ before do
+ described_class.base_uri(mattermost_url)
+ end
+
+ describe '#with session' do
+ let(:location) { 'http://location.tld' }
+ let!(:stub) do
+ WebMock.stub_request(:get, "#{mattermost_url}/api/v3/oauth/gitlab/login").
+ to_return(headers: { 'location' => location }, status: 307)
+ end
+
+ context 'without oauth uri' do
+ it 'makes a request to the oauth uri' do
+ expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
+ end
+ end
+
+ context 'with oauth_uri' do
+ let!(:doorkeeper) do
+ Doorkeeper::Application.create(
+ name: "GitLab Mattermost",
+ redirect_uri: "#{mattermost_url}/signup/gitlab/complete\n#{mattermost_url}/login/gitlab/complete",
+ scopes: "")
+ end
+
+ context 'without token_uri' do
+ it 'can not create a session' do
+ expect do
+ subject.with_session
+ end.to raise_error(Mattermost::NoSessionError)
+ end
+ end
+
+ context 'with token_uri' do
+ let(:state) { "state" }
+ let(:params) do
+ { response_type: "code",
+ client_id: doorkeeper.uid,
+ redirect_uri: "#{mattermost_url}/signup/gitlab/complete",
+ state: state }
+ end
+ let(:location) do
+ "#{gitlab_url}/oauth/authorize?#{URI.encode_www_form(params)}"
+ end
+
+ before do
+ WebMock.stub_request(:get, "#{mattermost_url}/signup/gitlab/complete").
+ with(query: hash_including({ 'state' => state })).
+ to_return do |request|
+ post "/oauth/token",
+ client_id: doorkeeper.uid,
+ client_secret: doorkeeper.secret,
+ redirect_uri: params[:redirect_uri],
+ grant_type: 'authorization_code',
+ code: request.uri.query_values['code']
+
+ if response.status == 200
+ { headers: { 'token' => 'thisworksnow' }, status: 202 }
+ end
+ end
+
+ WebMock.stub_request(:post, "#{mattermost_url}/api/v3/users/logout").
+ to_return(headers: { Authorization: 'token thisworksnow' }, status: 200)
+ end
+
+ it 'can setup a session' do
+ subject.with_session do |session|
+ end
+
+ expect(subject.token).not_to be_nil
+ end
+
+ it 'returns the value of the block' do
+ result = subject.with_session do |session|
+ "value"
+ end
+
+ expect(result).to eq("value")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index 14bc062ef12..e1877d5fde0 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -25,7 +25,7 @@ describe Notify do
it 'includes a link for user to set password' do
params = "reset_password_token=#{token}"
is_expected.to have_body_text(
- %r{http://localhost(:\d+)?/users/password/edit\?#{params}}
+ %r{http://#{Gitlab.config.gitlab.host}(:\d+)?/users/password/edit\?#{params}}
)
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index f5f3f58613d..b692142713f 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -50,7 +50,7 @@ describe Notify do
context 'when enabled email_author_in_body' do
before do
- allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true)
+ stub_application_setting(email_author_in_body: true)
end
it 'contains a link to note author' do
@@ -229,7 +229,7 @@ describe Notify do
context 'when enabled email_author_in_body' do
before do
- allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true)
+ stub_application_setting(email_author_in_body: true)
end
it 'contains a link to note author' do
@@ -401,7 +401,12 @@ describe Notify do
describe 'project access requested' do
context 'for a project in a user namespace' do
- let(:project) { create(:project, :public).tap { |p| p.team << [p.owner, :master, p.owner] } }
+ let(:project) do
+ create(:empty_project, :public, :access_requestable) do |project|
+ project.team << [project.owner, :master, project.owner]
+ end
+ end
+
let(:user) { create(:user) }
let(:project_member) do
project.request_access(user)
@@ -428,7 +433,7 @@ describe Notify do
context 'for a project in a group' do
let(:group_owner) { create(:user) }
let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } }
- let(:project) { create(:project, :public, namespace: group) }
+ let(:project) { create(:empty_project, :public, :access_requestable, namespace: group) }
let(:user) { create(:user) }
let(:project_member) do
project.request_access(user)
@@ -454,7 +459,7 @@ describe Notify do
end
describe 'project access denied' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:user) { create(:user) }
let(:project_member) do
project.request_access(user)
@@ -474,7 +479,7 @@ describe Notify do
end
describe 'project access changed' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:user) { create(:user) }
let(:project_member) { create(:project_member, project: project, user: user) }
subject { Notify.member_access_granted_email('project', project_member.id) }
@@ -602,7 +607,7 @@ describe Notify do
context 'when enabled email_author_in_body' do
before do
- allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true)
+ stub_application_setting(email_author_in_body: true)
end
it 'contains a link to note author' do
@@ -681,11 +686,84 @@ describe Notify do
end
end
end
+
+ context 'items that are noteable, emails for a note on a diff' do
+ let(:note_author) { create(:user, name: 'author_name') }
+
+ before :each do
+ allow(Note).to receive(:find).with(note.id).and_return(note)
+ end
+
+ shared_examples 'a note email on a diff' do |model|
+ let(:note) { create(model, project: project, author: note_author) }
+
+ it "includes diffs with character-level highlighting" do
+ is_expected.to have_body_text /<span class=\"p\">}<\/span><\/span>/
+ end
+
+ it 'contains a link to the diff file' do
+ is_expected.to have_body_text /#{note.diff_file.file_path}/
+ end
+
+ it_behaves_like 'it should have Gmail Actions links'
+
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(note_author.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
+
+ it 'is sent to the given recipient' do
+ is_expected.to deliver_to recipient.notification_email
+ end
+
+ it 'contains the message from the note' do
+ is_expected.to have_body_text /#{note.note}/
+ end
+
+ it 'does not contain note author' do
+ is_expected.not_to have_body_text /wrote\:/
+ end
+
+ context 'when enabled email_author_in_body' do
+ before do
+ stub_application_setting(email_author_in_body: true)
+ end
+
+ it 'contains a link to note author' do
+ is_expected.to have_body_text note.author_name
+ is_expected.to have_body_text /wrote\:/
+ end
+ end
+ end
+
+ describe 'on a commit' do
+ let(:commit) { project.commit }
+ let(:note) { create(:diff_note_on_commit) }
+
+ subject { Notify.note_commit_email(recipient.id, note.id) }
+
+ it_behaves_like 'a note email on a diff', :diff_note_on_commit
+ it_behaves_like 'it should show Gmail Actions View Commit link'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+ end
+
+ describe 'on a merge request' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:note) { create(:diff_note_on_merge_request) }
+
+ subject { Notify.note_merge_request_email(recipient.id, note.id) }
+
+ it_behaves_like 'a note email on a diff', :diff_note_on_merge_request
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like 'an unsubscribeable thread'
+ end
+ end
end
context 'for a group' do
describe 'group access requested' do
- let(:group) { create(:group) }
+ let(:group) { create(:group, :public, :access_requestable) }
let(:user) { create(:user) }
let(:group_member) do
group.request_access(user)
@@ -1094,4 +1172,38 @@ describe Notify do
is_expected.to have_body_text /#{diff_path}/
end
end
+
+ describe 'HTML emails setting' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:multipart_mail) { Notify.project_was_moved_email(project.id, user.id, "gitlab/gitlab") }
+
+ context 'when disabled' do
+ it 'only sends the text template' do
+ stub_application_setting(html_emails_enabled: false)
+
+ EmailTemplateInterceptor.delivering_email(multipart_mail)
+
+ expect(multipart_mail).to have_part_with('text/plain')
+ expect(multipart_mail).not_to have_part_with('text/html')
+ end
+ end
+
+ context 'when enabled' do
+ it 'sends a multipart message' do
+ stub_application_setting(html_emails_enabled: true)
+
+ EmailTemplateInterceptor.delivering_email(multipart_mail)
+
+ expect(multipart_mail).to have_part_with('text/plain')
+ expect(multipart_mail).to have_part_with('text/html')
+ end
+ end
+
+ matcher :have_part_with do |expected|
+ match do |actual|
+ actual.body.parts.any? { |part| part.content_type.try(:match, %r(#{expected})) }
+ end
+ end
+ end
end
diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
new file mode 100644
index 00000000000..6a93deb5412
--- /dev/null
+++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
@@ -0,0 +1,217 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20161124141322_migrate_process_commit_worker_jobs.rb')
+
+describe MigrateProcessCommitWorkerJobs do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:commit) { project.commit.raw.raw_commit }
+
+ describe 'Project' do
+ describe 'find_including_path' do
+ it 'returns Project instances' do
+ expect(described_class::Project.find_including_path(project.id)).
+ to be_an_instance_of(described_class::Project)
+ end
+
+ it 'selects the full path for every Project' do
+ migration_project = described_class::Project.
+ find_including_path(project.id)
+
+ expect(migration_project[:path_with_namespace]).
+ to eq(project.path_with_namespace)
+ end
+ end
+
+ describe '#repository_storage_path' do
+ it 'returns the storage path for the repository' do
+ migration_project = described_class::Project.
+ find_including_path(project.id)
+
+ expect(File.directory?(migration_project.repository_storage_path)).
+ to eq(true)
+ end
+ end
+
+ describe '#repository_path' do
+ it 'returns the path to the repository' do
+ migration_project = described_class::Project.
+ find_including_path(project.id)
+
+ expect(File.directory?(migration_project.repository_path)).to eq(true)
+ end
+ end
+
+ describe '#repository' do
+ it 'returns a Rugged::Repository' do
+ migration_project = described_class::Project.
+ find_including_path(project.id)
+
+ expect(migration_project.repository).
+ to be_an_instance_of(Rugged::Repository)
+ end
+ end
+ end
+
+ describe '#up', :redis do
+ let(:migration) { described_class.new }
+
+ def job_count
+ Sidekiq.redis { |r| r.llen('queue:process_commit') }
+ end
+
+ def pop_job
+ JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') })
+ end
+
+ before do
+ Sidekiq.redis do |redis|
+ job = JSON.dump(args: [project.id, user.id, commit.oid])
+ redis.lpush('queue:process_commit', job)
+ end
+ end
+
+ it 'skips jobs using a project that no longer exists' do
+ allow(described_class::Project).to receive(:find_including_path).
+ with(project.id).
+ and_return(nil)
+
+ migration.up
+
+ expect(job_count).to eq(0)
+ end
+
+ it 'skips jobs using commits that no longer exist' do
+ allow_any_instance_of(Rugged::Repository).to receive(:lookup).
+ with(commit.oid).
+ and_raise(Rugged::OdbError)
+
+ migration.up
+
+ expect(job_count).to eq(0)
+ end
+
+ it 'inserts migrated jobs back into the queue' do
+ migration.up
+
+ expect(job_count).to eq(1)
+ end
+
+ it 'encodes data to UTF-8' do
+ allow_any_instance_of(Rugged::Repository).to receive(:lookup).
+ with(commit.oid).
+ and_return(commit)
+
+ allow(commit).to receive(:message).
+ and_return('김치'.force_encoding('BINARY'))
+
+ migration.up
+
+ job = pop_job
+
+ # We don't care so much about what is being stored, instead we just want
+ # to make sure the encoding is right so that JSON encoding the data
+ # doesn't produce any errors.
+ expect(job['args'][2]['message'].encoding).to eq(Encoding::UTF_8)
+ end
+
+ context 'a migrated job' do
+ let(:job) do
+ migration.up
+ pop_job
+ end
+
+ let(:commit_hash) do
+ job['args'][2]
+ end
+
+ it 'includes the project ID' do
+ expect(job['args'][0]).to eq(project.id)
+ end
+
+ it 'includes the user ID' do
+ expect(job['args'][1]).to eq(user.id)
+ end
+
+ it 'includes the commit ID' do
+ expect(commit_hash['id']).to eq(commit.oid)
+ end
+
+ it 'includes the commit message' do
+ expect(commit_hash['message']).to eq(commit.message)
+ end
+
+ it 'includes the parent IDs' do
+ expect(commit_hash['parent_ids']).to eq(commit.parent_ids)
+ end
+
+ it 'includes the author date' do
+ expect(commit_hash['authored_date']).to eq(commit.author[:time].to_s)
+ end
+
+ it 'includes the author name' do
+ expect(commit_hash['author_name']).to eq(commit.author[:name])
+ end
+
+ it 'includes the author Email' do
+ expect(commit_hash['author_email']).to eq(commit.author[:email])
+ end
+
+ it 'includes the commit date' do
+ expect(commit_hash['committed_date']).to eq(commit.committer[:time].to_s)
+ end
+
+ it 'includes the committer name' do
+ expect(commit_hash['committer_name']).to eq(commit.committer[:name])
+ end
+
+ it 'includes the committer Email' do
+ expect(commit_hash['committer_email']).to eq(commit.committer[:email])
+ end
+ end
+ end
+
+ describe '#down', :redis do
+ let(:migration) { described_class.new }
+
+ def job_count
+ Sidekiq.redis { |r| r.llen('queue:process_commit') }
+ end
+
+ before do
+ Sidekiq.redis do |redis|
+ job = JSON.dump(args: [project.id, user.id, commit.oid])
+ redis.lpush('queue:process_commit', job)
+
+ migration.up
+ end
+ end
+
+ it 'pushes migrated jobs back into the queue' do
+ migration.down
+
+ expect(job_count).to eq(1)
+ end
+
+ context 'a migrated job' do
+ let(:job) do
+ migration.down
+
+ JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') })
+ end
+
+ it 'includes the project ID' do
+ expect(job['args'][0]).to eq(project.id)
+ end
+
+ it 'includes the user ID' do
+ expect(job['args'][1]).to eq(user.id)
+ end
+
+ it 'includes the commit SHA' do
+ expect(job['args'][2]).to eq(commit.oid)
+ end
+ end
+ end
+end
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 02d6263094a..219db365a91 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe BroadcastMessage, models: true do
- subject { create(:broadcast_message) }
+ subject { build(:broadcast_message) }
it { is_expected.to be_valid }
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index ae185de9ca3..6f1c2ae0fd8 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -87,6 +87,26 @@ describe Ci::Build, models: true do
end
end
+ describe '#persisted_environment' do
+ before do
+ @environment = create(:environment, project: project, name: "foo-#{project.default_branch}")
+ end
+
+ subject { build.persisted_environment }
+
+ context 'referenced literally' do
+ let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") }
+
+ it { is_expected.to eq(@environment) }
+ end
+
+ context 'referenced with a variable' do
+ let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_BUILD_REF_NAME") }
+
+ it { is_expected.to eq(@environment) }
+ end
+ end
+
describe '#trace' do
it { expect(build.trace).to be_nil }
@@ -254,6 +274,24 @@ describe Ci::Build, models: true do
end
end
+ describe '#ref_slug' do
+ {
+ 'master' => 'master',
+ '1-foo' => '1-foo',
+ 'fix/1-foo' => 'fix-1-foo',
+ 'fix-1-foo' => 'fix-1-foo',
+ 'a' * 63 => 'a' * 63,
+ 'a' * 64 => 'a' * 63,
+ 'FOO' => 'foo',
+ }.each do |ref, slug|
+ it "transforms #{ref} to #{slug}" do
+ build.ref = ref
+
+ expect(build.ref_slug).to eq(slug)
+ end
+ end
+ end
+
describe '#variables' do
let(:container_registry_enabled) { false }
let(:predefined_variables) do
@@ -265,6 +303,7 @@ describe Ci::Build, models: true do
{ key: 'CI_BUILD_REF', value: build.sha, public: true },
{ key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true },
{ key: 'CI_BUILD_REF_NAME', value: 'master', public: true },
+ { key: 'CI_BUILD_REF_SLUG', value: 'master', public: true },
{ key: 'CI_BUILD_NAME', value: 'test', public: true },
{ key: 'CI_BUILD_STAGE', value: 'test', public: true },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
@@ -309,6 +348,22 @@ describe Ci::Build, models: true do
it { user_variables.each { |v| is_expected.to include(v) } }
end
+ context 'when build has an environment' do
+ before do
+ build.update(environment: 'production')
+ create(:environment, project: build.project, name: 'production', slug: 'prod-slug')
+ end
+
+ let(:environment_variables) do
+ [
+ { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true },
+ { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true }
+ ]
+ end
+
+ it { environment_variables.each { |v| is_expected.to include(v) } }
+ end
+
context 'when build started manually' do
before do
build.update_attributes(when: :manual)
@@ -451,6 +506,17 @@ describe Ci::Build, models: true do
it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true }) }
end
+ context 'when build is for a deployment' do
+ let(:deployment_variable) { { key: 'KUBERNETES_TOKEN', value: 'TOKEN', public: false } }
+
+ before do
+ build.environment = 'production'
+ allow(project).to receive(:deployment_variables).and_return([deployment_variable])
+ end
+
+ it { is_expected.to include(deployment_variable) }
+ end
+
context 'returns variables in valid order' do
before do
allow(build).to receive(:predefined_variables) { ['predefined'] }
@@ -730,8 +796,8 @@ describe Ci::Build, models: true do
pipeline2 = create(:ci_pipeline, project: project)
@build2 = create(:ci_build, pipeline: pipeline2)
- commits = [double(id: pipeline.sha), double(id: pipeline2.sha)]
- allow(@merge_request).to receive(:commits).and_return(commits)
+ allow(@merge_request).to receive(:commits_sha).
+ and_return([pipeline.sha, pipeline2.sha])
allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request])
end
@@ -899,21 +965,87 @@ describe Ci::Build, models: true do
end
end
+ describe '#cancelable?' do
+ subject { build }
+
+ context 'when build is cancelable' do
+ context 'when build is pending' do
+ it { is_expected.to be_cancelable }
+ end
+
+ context 'when build is running' do
+ before do
+ build.run!
+ end
+
+ it { is_expected.to be_cancelable }
+ end
+ end
+
+ context 'when build is not cancelable' do
+ context 'when build is successful' do
+ before do
+ build.success!
+ end
+
+ it { is_expected.not_to be_cancelable }
+ end
+
+ context 'when build is failed' do
+ before do
+ build.drop!
+ end
+
+ it { is_expected.not_to be_cancelable }
+ end
+ end
+ end
+
describe '#retryable?' do
- context 'when build is running' do
- before do
- build.run!
+ subject { build }
+
+ context 'when build is retryable' do
+ context 'when build is successful' do
+ before do
+ build.success!
+ end
+
+ it { is_expected.to be_retryable }
end
- it { expect(build).not_to be_retryable }
+ context 'when build is failed' do
+ before do
+ build.drop!
+ end
+
+ it { is_expected.to be_retryable }
+ end
+
+ context 'when build is canceled' do
+ before do
+ build.cancel!
+ end
+
+ it { is_expected.to be_retryable }
+ end
end
- context 'when build is finished' do
- before do
- build.success!
+ context 'when build is not retryable' do
+ context 'when build is running' do
+ before do
+ build.run!
+ end
+
+ it { is_expected.not_to be_retryable }
end
- it { expect(build).to be_retryable }
+ context 'when build is skipped' do
+ before do
+ build.skip!
+ end
+
+ it { is_expected.not_to be_retryable }
+ end
end
end
@@ -1052,4 +1184,141 @@ describe Ci::Build, models: true do
end
end
end
+
+ describe '#has_environment?' do
+ subject { build.has_environment? }
+
+ context 'when environment is defined' do
+ before do
+ build.update(environment: 'review')
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when environment is not defined' do
+ before do
+ build.update(environment: nil)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#starts_environment?' do
+ subject { build.starts_environment? }
+
+ context 'when environment is defined' do
+ before do
+ build.update(environment: 'review')
+ end
+
+ context 'no action is defined' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'and start action is defined' do
+ before do
+ build.update(options: { environment: { action: 'start' } } )
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when environment is not defined' do
+ before do
+ build.update(environment: nil)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#stops_environment?' do
+ subject { build.stops_environment? }
+
+ context 'when environment is defined' do
+ before do
+ build.update(environment: 'review')
+ end
+
+ context 'no action is defined' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'and stop action is defined' do
+ before do
+ build.update(options: { environment: { action: 'stop' } } )
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when environment is not defined' do
+ before do
+ build.update(environment: nil)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#last_deployment' do
+ subject { build.last_deployment }
+
+ context 'when multiple deployments are created' do
+ let!(:deployment1) { create(:deployment, deployable: build) }
+ let!(:deployment2) { create(:deployment, deployable: build) }
+
+ it 'returns the latest one' do
+ is_expected.to eq(deployment2)
+ end
+ end
+ end
+
+ describe '#outdated_deployment?' do
+ subject { build.outdated_deployment? }
+
+ context 'when build succeeded' do
+ let(:build) { create(:ci_build, :success) }
+ let!(:deployment) { create(:deployment, deployable: build) }
+
+ context 'current deployment is latest' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'current deployment is not latest on environment' do
+ let!(:deployment2) { create(:deployment, environment: deployment.environment) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when build failed' do
+ let(:build) { create(:ci_build, :failed) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#expanded_environment_name' do
+ subject { build.expanded_environment_name }
+
+ context 'when environment uses variables' do
+ let(:build) { create(:ci_build, ref: 'master', environment: 'review/$CI_BUILD_REF_NAME') }
+
+ it { is_expected.to eq('review/master') }
+ end
+ end
+
+ describe '#detailed_status' do
+ let(:user) { create(:user) }
+
+ it 'returns a detailed status' do
+ expect(build.detailed_status(user))
+ .to be_a Gitlab::Ci::Status::Build::Cancelable
+ end
+ end
end
diff --git a/spec/models/chat_name_spec.rb b/spec/models/chat_name_spec.rb
new file mode 100644
index 00000000000..b02971cab82
--- /dev/null
+++ b/spec/models/chat_name_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe ChatName, models: true do
+ subject { create(:chat_name) }
+
+ it { is_expected.to belong_to(:service) }
+ it { is_expected.to belong_to(:user) }
+
+ it { is_expected.to validate_presence_of(:user) }
+ it { is_expected.to validate_presence_of(:service) }
+ it { is_expected.to validate_presence_of(:team_id) }
+ it { is_expected.to validate_presence_of(:chat_id) }
+
+ it { is_expected.to validate_uniqueness_of(:user_id).scoped_to(:service_id) }
+ it { is_expected.to validate_uniqueness_of(:chat_id).scoped_to(:service_id, :team_id) }
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index a37a00f461a..a7e90c8a381 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -4,6 +4,12 @@ describe Ci::Build, models: true do
let(:build) { create(:ci_build) }
let(:test_trace) { 'This is a test' }
+ it { is_expected.to belong_to(:runner) }
+ it { is_expected.to belong_to(:trigger_request) }
+ it { is_expected.to belong_to(:erased_by) }
+
+ it { is_expected.to have_many(:deployments) }
+
describe '#trace' do
it 'obfuscates project runners token' do
allow(build).to receive(:raw_trace).and_return("Test: #{build.project.runners_token}")
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 5eb14dc6bd2..52dd41065e9 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Ci::Pipeline, models: true do
+ include EmailHelpers
+
let(:project) { FactoryGirl.create :empty_project }
let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, status: 'created', project: project }
@@ -18,8 +20,6 @@ describe Ci::Pipeline, models: true do
it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :short_sha }
- it { is_expected.to delegate_method(:stages).to(:statuses) }
-
describe '#valid_commit_sha' do
context 'commit.sha can not start with 00000000' do
before do
@@ -123,16 +123,55 @@ describe Ci::Pipeline, models: true do
end
describe '#stages' do
- let(:pipeline2) { FactoryGirl.create :ci_pipeline, project: project }
- subject { CommitStatus.where(pipeline: [pipeline, pipeline2]).stages }
-
before do
- FactoryGirl.create :ci_build, pipeline: pipeline2, stage: 'test', stage_idx: 1
- FactoryGirl.create :ci_build, pipeline: pipeline, stage: 'build', stage_idx: 0
+ create(:commit_status, pipeline: pipeline, stage: 'build', name: 'linux', stage_idx: 0, status: 'success')
+ create(:commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'failed')
+ create(:commit_status, pipeline: pipeline, stage: 'deploy', name: 'staging', stage_idx: 2, status: 'running')
+ create(:commit_status, pipeline: pipeline, stage: 'test', name: 'rspec', stage_idx: 1, status: 'success')
+ end
+
+ subject { pipeline.stages }
+
+ context 'stages list' do
+ it 'returns ordered list of stages' do
+ expect(subject.map(&:name)).to eq(%w[build test deploy])
+ end
end
- it 'return all stages' do
- is_expected.to eq(%w(build test))
+ it 'returns a valid number of stages' do
+ expect(pipeline.stages_count).to eq(3)
+ end
+
+ it 'returns a valid names of stages' do
+ expect(pipeline.stages_name).to eq(['build', 'test', 'deploy'])
+ end
+
+ context 'stages with statuses' do
+ let(:statuses) do
+ subject.map do |stage|
+ [stage.name, stage.status]
+ end
+ end
+
+ it 'returns list of stages with statuses' do
+ expect(statuses).to eq([['build', 'failed'],
+ ['test', 'success'],
+ ['deploy', 'running']
+ ])
+ end
+
+ context 'when build is retried' do
+ before do
+ create(:commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'success')
+ end
+
+ it 'ignores the previous state' do
+ expect(statuses).to eq([['build', 'success'],
+ ['test', 'success'],
+ ['deploy', 'running']
+ ])
+ end
+ end
end
end
@@ -342,6 +381,65 @@ describe Ci::Pipeline, models: true do
end
end
+ shared_context 'with some outdated pipelines' do
+ before do
+ create_pipeline(:canceled, 'ref', 'A')
+ create_pipeline(:success, 'ref', 'A')
+ create_pipeline(:failed, 'ref', 'B')
+ create_pipeline(:skipped, 'feature', 'C')
+ end
+
+ def create_pipeline(status, ref, sha)
+ create(:ci_empty_pipeline, status: status, ref: ref, sha: sha)
+ end
+ end
+
+ describe '.latest' do
+ include_context 'with some outdated pipelines'
+
+ context 'when no ref is specified' do
+ let(:pipelines) { described_class.latest.all }
+
+ it 'returns the latest pipeline for the same ref and different sha' do
+ expect(pipelines.map(&:sha)).to contain_exactly('A', 'B', 'C')
+ expect(pipelines.map(&:status)).
+ to contain_exactly('success', 'failed', 'skipped')
+ end
+ end
+
+ context 'when ref is specified' do
+ let(:pipelines) { described_class.latest('ref').all }
+
+ it 'returns the latest pipeline for ref and different sha' do
+ expect(pipelines.map(&:sha)).to contain_exactly('A', 'B')
+ expect(pipelines.map(&:status)).
+ to contain_exactly('success', 'failed')
+ end
+ end
+ end
+
+ describe '.latest_status' do
+ include_context 'with some outdated pipelines'
+
+ context 'when no ref is specified' do
+ let(:latest_status) { described_class.latest_status }
+
+ it 'returns the latest status for the same ref and different sha' do
+ expect(latest_status).to eq(described_class.latest.status)
+ expect(latest_status).to eq('failed')
+ end
+ end
+
+ context 'when ref is specified' do
+ let(:latest_status) { described_class.latest_status('ref') }
+
+ it 'returns the latest status for ref and different sha' do
+ expect(latest_status).to eq(described_class.latest_status('ref'))
+ expect(latest_status).to eq('failed')
+ end
+ end
+ end
+
describe '#status' do
let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') }
@@ -402,6 +500,234 @@ describe Ci::Pipeline, models: true do
end
end
+ describe '#detailed_status' do
+ let(:user) { create(:user) }
+
+ subject { pipeline.detailed_status(user) }
+
+ context 'when pipeline is created' do
+ let(:pipeline) { create(:ci_pipeline, status: :created) }
+
+ it 'returns detailed status for created pipeline' do
+ expect(subject.text).to eq 'created'
+ end
+ end
+
+ context 'when pipeline is pending' do
+ let(:pipeline) { create(:ci_pipeline, status: :pending) }
+
+ it 'returns detailed status for pending pipeline' do
+ expect(subject.text).to eq 'pending'
+ end
+ end
+
+ context 'when pipeline is running' do
+ let(:pipeline) { create(:ci_pipeline, status: :running) }
+
+ it 'returns detailed status for running pipeline' do
+ expect(subject.text).to eq 'running'
+ end
+ end
+
+ context 'when pipeline is successful' do
+ let(:pipeline) { create(:ci_pipeline, status: :success) }
+
+ it 'returns detailed status for successful pipeline' do
+ expect(subject.text).to eq 'passed'
+ end
+ end
+
+ context 'when pipeline is failed' do
+ let(:pipeline) { create(:ci_pipeline, status: :failed) }
+
+ it 'returns detailed status for failed pipeline' do
+ expect(subject.text).to eq 'failed'
+ end
+ end
+
+ context 'when pipeline is canceled' do
+ let(:pipeline) { create(:ci_pipeline, status: :canceled) }
+
+ it 'returns detailed status for canceled pipeline' do
+ expect(subject.text).to eq 'canceled'
+ end
+ end
+
+ context 'when pipeline is skipped' do
+ let(:pipeline) { create(:ci_pipeline, status: :skipped) }
+
+ it 'returns detailed status for skipped pipeline' do
+ expect(subject.text).to eq 'skipped'
+ end
+ end
+
+ context 'when pipeline is successful but with warnings' do
+ let(:pipeline) { create(:ci_pipeline, status: :success) }
+
+ before do
+ create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline)
+ end
+
+ it 'retruns detailed status for successful pipeline with warnings' do
+ expect(subject.label).to eq 'passed with warnings'
+ end
+ end
+ end
+
+ describe '#cancelable?' do
+ %i[created running pending].each do |status0|
+ context "when there is a build #{status0}" do
+ before do
+ create(:ci_build, status0, pipeline: pipeline)
+ end
+
+ it 'is cancelable' do
+ expect(pipeline.cancelable?).to be_truthy
+ end
+ end
+
+ context "when there is an external job #{status0}" do
+ before do
+ create(:generic_commit_status, status0, pipeline: pipeline)
+ end
+
+ it 'is cancelable' do
+ expect(pipeline.cancelable?).to be_truthy
+ end
+ end
+
+ %i[success failed canceled].each do |status1|
+ context "when there are generic_commit_status jobs for #{status0} and #{status1}" do
+ before do
+ create(:generic_commit_status, status0, pipeline: pipeline)
+ create(:generic_commit_status, status1, pipeline: pipeline)
+ end
+
+ it 'is cancelable' do
+ expect(pipeline.cancelable?).to be_truthy
+ end
+ end
+
+ context "when there are generic_commit_status and ci_build jobs for #{status0} and #{status1}" do
+ before do
+ create(:generic_commit_status, status0, pipeline: pipeline)
+ create(:ci_build, status1, pipeline: pipeline)
+ end
+
+ it 'is cancelable' do
+ expect(pipeline.cancelable?).to be_truthy
+ end
+ end
+
+ context "when there are ci_build jobs for #{status0} and #{status1}" do
+ before do
+ create(:ci_build, status0, pipeline: pipeline)
+ create(:ci_build, status1, pipeline: pipeline)
+ end
+
+ it 'is cancelable' do
+ expect(pipeline.cancelable?).to be_truthy
+ end
+ end
+ end
+ end
+
+ %i[success failed canceled].each do |status|
+ context "when there is a build #{status}" do
+ before do
+ create(:ci_build, status, pipeline: pipeline)
+ end
+
+ it 'is not cancelable' do
+ expect(pipeline.cancelable?).to be_falsey
+ end
+ end
+
+ context "when there is an external job #{status}" do
+ before do
+ create(:generic_commit_status, status, pipeline: pipeline)
+ end
+
+ it 'is not cancelable' do
+ expect(pipeline.cancelable?).to be_falsey
+ end
+ end
+ end
+ end
+
+ describe '#cancel_running' do
+ let(:latest_status) { pipeline.statuses.pluck(:status) }
+
+ context 'when there is a running external job and created build' do
+ before do
+ create(:ci_build, :running, pipeline: pipeline)
+ create(:generic_commit_status, :running, pipeline: pipeline)
+
+ pipeline.cancel_running
+ end
+
+ it 'cancels both jobs' do
+ expect(latest_status).to contain_exactly('canceled', 'canceled')
+ end
+ end
+
+ context 'when builds are in different stages' do
+ before do
+ create(:ci_build, :running, stage_idx: 0, pipeline: pipeline)
+ create(:ci_build, :running, stage_idx: 1, pipeline: pipeline)
+
+ pipeline.cancel_running
+ end
+
+ it 'cancels both jobs' do
+ expect(latest_status).to contain_exactly('canceled', 'canceled')
+ end
+ end
+ end
+
+ describe '#retry_failed' do
+ let(:latest_status) { pipeline.statuses.latest.pluck(:status) }
+
+ context 'when there is a failed build and failed external status' do
+ before do
+ create(:ci_build, :failed, name: 'build', pipeline: pipeline)
+ create(:generic_commit_status, :failed, name: 'jenkins', pipeline: pipeline)
+
+ pipeline.retry_failed(create(:user))
+ end
+
+ it 'retries only build' do
+ expect(latest_status).to contain_exactly('pending', 'failed')
+ end
+ end
+
+ context 'when builds are in different stages' do
+ before do
+ create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline)
+ create(:ci_build, :failed, name: 'jenkins', stage_idx: 1, pipeline: pipeline)
+
+ pipeline.retry_failed(create(:user))
+ end
+
+ it 'retries both builds' do
+ expect(latest_status).to contain_exactly('pending', 'pending')
+ end
+ end
+
+ context 'when there are canceled and failed' do
+ before do
+ create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline)
+ create(:ci_build, :canceled, name: 'jenkins', stage_idx: 1, pipeline: pipeline)
+
+ pipeline.retry_failed(create(:user))
+ end
+
+ it 'retries both builds' do
+ expect(latest_status).to contain_exactly('pending', 'pending')
+ end
+ end
+ end
+
describe '#execute_hooks' do
let!(:build_a) { create_build('a', 0) }
let!(:build_b) { create_build('b', 1) }
@@ -524,4 +850,81 @@ describe Ci::Pipeline, models: true do
expect(pipeline.merge_requests).to be_empty
end
end
+
+ describe 'notifications when pipeline success or failed' do
+ let(:project) { create(:project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: project.commit('master').sha,
+ user: create(:user))
+ end
+
+ before do
+ reset_delivered_emails!
+
+ project.team << [pipeline.user, Gitlab::Access::DEVELOPER]
+
+ perform_enqueued_jobs do
+ pipeline.enqueue
+ pipeline.run
+ end
+ end
+
+ shared_examples 'sending a notification' do
+ it 'sends an email' do
+ should_only_email(pipeline.user, kind: :bcc)
+ end
+ end
+
+ shared_examples 'not sending any notification' do
+ it 'does not send any email' do
+ should_not_email_anyone
+ end
+ end
+
+ context 'with success pipeline' do
+ before do
+ perform_enqueued_jobs do
+ pipeline.succeed
+ end
+ end
+
+ it_behaves_like 'sending a notification'
+ end
+
+ context 'with failed pipeline' do
+ before do
+ perform_enqueued_jobs do
+ create(:ci_build, :failed, pipeline: pipeline)
+ create(:generic_commit_status, :failed, pipeline: pipeline)
+
+ pipeline.drop
+ end
+ end
+
+ it_behaves_like 'sending a notification'
+ end
+
+ context 'with skipped pipeline' do
+ before do
+ perform_enqueued_jobs do
+ pipeline.skip
+ end
+ end
+
+ it_behaves_like 'not sending any notification'
+ end
+
+ context 'with cancelled pipeline' do
+ before do
+ perform_enqueued_jobs do
+ pipeline.cancel
+ end
+ end
+
+ it_behaves_like 'not sending any notification'
+ end
+ end
end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
new file mode 100644
index 00000000000..8fff38f7cda
--- /dev/null
+++ b/spec/models/ci/stage_spec.rb
@@ -0,0 +1,135 @@
+require 'spec_helper'
+
+describe Ci::Stage, models: true do
+ let(:stage) { build(:ci_stage) }
+ let(:pipeline) { stage.pipeline }
+ let(:stage_name) { stage.name }
+
+ describe '#expectations' do
+ subject { stage }
+
+ it { is_expected.to include_module(StaticModel) }
+
+ it { is_expected.to respond_to(:pipeline) }
+ it { is_expected.to respond_to(:name) }
+
+ it { is_expected.to delegate_method(:project).to(:pipeline) }
+ end
+
+ describe '#statuses' do
+ let!(:stage_build) { create_job(:ci_build) }
+ let!(:commit_status) { create_job(:commit_status) }
+ let!(:other_build) { create_job(:ci_build, stage: 'other stage') }
+
+ subject { stage.statuses }
+
+ it "returns only matching statuses" do
+ is_expected.to contain_exactly(stage_build, commit_status)
+ end
+ end
+
+ describe '#builds' do
+ let!(:stage_build) { create_job(:ci_build) }
+ let!(:commit_status) { create_job(:commit_status) }
+
+ subject { stage.builds }
+
+ it "returns only builds" do
+ is_expected.to contain_exactly(stage_build)
+ end
+ end
+
+ describe '#status' do
+ subject { stage.status }
+
+ context 'if status is already defined' do
+ let(:stage) { build(:ci_stage, status: 'success') }
+
+ it "returns defined status" do
+ is_expected.to eq('success')
+ end
+ end
+
+ context 'if status has to be calculated' do
+ let!(:stage_build) { create_job(:ci_build, status: :failed) }
+
+ it "returns status of a build" do
+ is_expected.to eq('failed')
+ end
+
+ context 'and builds are retried' do
+ let!(:new_build) { create_job(:ci_build, status: :success) }
+
+ it "returns status of latest build" do
+ is_expected.to eq('success')
+ end
+ end
+ end
+ end
+
+ describe '#detailed_status' do
+ let(:user) { create(:user) }
+
+ subject { stage.detailed_status(user) }
+
+ context 'when build is created' do
+ let!(:stage_build) { create_job(:ci_build, status: :created) }
+
+ it 'returns detailed status for created stage' do
+ expect(subject.text).to eq 'created'
+ end
+ end
+
+ context 'when build is pending' do
+ let!(:stage_build) { create_job(:ci_build, status: :pending) }
+
+ it 'returns detailed status for pending stage' do
+ expect(subject.text).to eq 'pending'
+ end
+ end
+
+ context 'when build is running' do
+ let!(:stage_build) { create_job(:ci_build, status: :running) }
+
+ it 'returns detailed status for running stage' do
+ expect(subject.text).to eq 'running'
+ end
+ end
+
+ context 'when build is successful' do
+ let!(:stage_build) { create_job(:ci_build, status: :success) }
+
+ it 'returns detailed status for successful stage' do
+ expect(subject.text).to eq 'passed'
+ end
+ end
+
+ context 'when build is failed' do
+ let!(:stage_build) { create_job(:ci_build, status: :failed) }
+
+ it 'returns detailed status for failed stage' do
+ expect(subject.text).to eq 'failed'
+ end
+ end
+
+ context 'when build is canceled' do
+ let!(:stage_build) { create_job(:ci_build, status: :canceled) }
+
+ it 'returns detailed status for canceled stage' do
+ expect(subject.text).to eq 'canceled'
+ end
+ end
+
+ context 'when build is skipped' do
+ let!(:stage_build) { create_job(:ci_build, status: :skipped) }
+
+ it 'returns detailed status for skipped stage' do
+ expect(subject.text).to eq 'skipped'
+ end
+ end
+ end
+
+ def create_job(type, status: 'success', stage: stage_name)
+ create(type, pipeline: pipeline, stage: stage, status: status)
+ end
+end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index 4e7833c3162..bee9f714849 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -5,6 +5,13 @@ describe Ci::Variable, models: true do
let(:secret_value) { 'secret' }
+ it { is_expected.to validate_presence_of(:key) }
+ it { is_expected.to validate_uniqueness_of(:key).scoped_to(:gl_project_id) }
+ it { is_expected.to validate_length_of(:key).is_at_most(255) }
+ it { is_expected.to allow_value('foo').for(:key) }
+ it { is_expected.not_to allow_value('foo bar').for(:key) }
+ it { is_expected.not_to allow_value('foo/bar').for(:key) }
+
before :each do
subject.value = secret_value
end
diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb
index c41359b55a3..30782ca75a0 100644
--- a/spec/models/commit_range_spec.rb
+++ b/spec/models/commit_range_spec.rb
@@ -45,7 +45,7 @@ describe CommitRange, models: true do
end
describe '#to_reference' do
- let(:cross) { create(:project) }
+ let(:cross) { create(:empty_project, namespace: project.namespace) }
it 'returns a String reference to the object' do
expect(range.to_reference).to eq "#{full_sha_from}...#{full_sha_to}"
@@ -56,12 +56,12 @@ describe CommitRange, models: true do
end
it 'supports a cross-project reference' do
- expect(range.to_reference(cross)).to eq "#{project.to_reference}@#{full_sha_from}...#{full_sha_to}"
+ expect(range.to_reference(cross)).to eq "#{project.path}@#{full_sha_from}...#{full_sha_to}"
end
end
describe '#reference_link_text' do
- let(:cross) { create(:project) }
+ let(:cross) { create(:empty_project, namespace: project.namespace) }
it 'returns a String reference to the object' do
expect(range.reference_link_text).to eq "#{sha_from}...#{sha_to}"
@@ -72,7 +72,7 @@ describe CommitRange, models: true do
end
it 'supports a cross-project reference' do
- expect(range.reference_link_text(cross)).to eq "#{project.to_reference}@#{sha_from}...#{sha_to}"
+ expect(range.reference_link_text(cross)).to eq "#{project.path}@#{sha_from}...#{sha_to}"
end
end
@@ -137,26 +137,25 @@ describe CommitRange, models: true do
end
describe '#has_been_reverted?' do
- it 'returns true if the commit has been reverted' do
- issue = create(:issue)
+ let(:issue) { create(:issue) }
+ let(:user) { issue.author }
+ it 'returns true if the commit has been reverted' do
create(:note_on_issue,
noteable: issue,
system: true,
- note: commit1.revert_description,
+ note: commit1.revert_description(user),
project: issue.project)
expect_any_instance_of(Commit).to receive(:reverts_commit?).
- with(commit1).
+ with(commit1, user).
and_return(true)
- expect(commit1.has_been_reverted?(nil, issue)).to eq(true)
+ expect(commit1.has_been_reverted?(user, issue)).to eq(true)
end
it 'returns false a commit has not been reverted' do
- issue = create(:issue)
-
- expect(commit1.has_been_reverted?(nil, issue)).to eq(false)
+ expect(commit1.has_been_reverted?(user, issue)).to eq(false)
end
end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index e3bb3482d67..74b50d2908d 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -34,24 +34,30 @@ describe Commit, models: true do
end
describe '#to_reference' do
+ let(:project) { create(:project, path: 'sample-project') }
+ let(:commit) { project.commit }
+
it 'returns a String reference to the object' do
expect(commit.to_reference).to eq commit.id
end
it 'supports a cross-project reference' do
- cross = double('project')
- expect(commit.to_reference(cross)).to eq "#{project.to_reference}@#{commit.id}"
+ another_project = build(:project, name: 'another-project', namespace: project.namespace)
+ expect(commit.to_reference(another_project)).to eq "sample-project@#{commit.id}"
end
end
describe '#reference_link_text' do
+ let(:project) { create(:project, path: 'sample-project') }
+ let(:commit) { project.commit }
+
it 'returns a String reference to the object' do
expect(commit.reference_link_text).to eq commit.short_id
end
it 'supports a cross-project reference' do
- cross = double('project')
- expect(commit.reference_link_text(cross)).to eq "#{project.to_reference}@#{commit.short_id}"
+ another_project = build(:project, name: 'another-project', namespace: project.namespace)
+ expect(commit.reference_link_text(another_project)).to eq "sample-project@#{commit.short_id}"
end
end
@@ -173,25 +179,26 @@ eos
describe '#reverts_commit?' do
let(:another_commit) { double(:commit, revert_description: "This reverts commit #{commit.sha}") }
+ let(:user) { commit.author }
- it { expect(commit.reverts_commit?(another_commit)).to be_falsy }
+ it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy }
context 'commit has no description' do
before { allow(commit).to receive(:description?).and_return(false) }
- it { expect(commit.reverts_commit?(another_commit)).to be_falsy }
+ it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy }
end
context "another_commit's description does not revert commit" do
before { allow(commit).to receive(:description).and_return("Foo Bar") }
- it { expect(commit.reverts_commit?(another_commit)).to be_falsy }
+ it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy }
end
context "another_commit's description reverts commit" do
before { allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar") }
- it { expect(commit.reverts_commit?(another_commit)).to be_truthy }
+ it { expect(commit.reverts_commit?(another_commit, user)).to be_truthy }
end
context "another_commit's description reverts merged merge request" do
@@ -201,28 +208,24 @@ eos
allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar")
end
- it { expect(commit.reverts_commit?(another_commit)).to be_truthy }
+ it { expect(commit.reverts_commit?(another_commit, user)).to be_truthy }
end
end
describe '#status' do
- context 'without arguments for compound status' do
- shared_examples 'giving the status from pipeline' do
- it do
- expect(commit.status).to eq(Ci::Pipeline.status)
- end
- end
-
- context 'with pipelines' do
- let!(:pipeline) do
- create(:ci_empty_pipeline, project: project, sha: commit.sha)
+ context 'without ref argument' do
+ before do
+ %w[success failed created pending].each do |status|
+ create(:ci_empty_pipeline,
+ project: project,
+ sha: commit.sha,
+ status: status)
end
-
- it_behaves_like 'giving the status from pipeline'
end
- context 'without pipelines' do
- it_behaves_like 'giving the status from pipeline'
+ it 'gives compound status from latest pipelines' do
+ expect(commit.status).to eq(Ci::Pipeline.latest_status)
+ expect(commit.status).to eq('pending')
end
end
@@ -248,8 +251,9 @@ eos
expect(commit.status('fix')).to eq(pipeline_from_fix.status)
end
- it 'gives compound status if ref is nil' do
- expect(commit.status(nil)).to eq(commit.status)
+ it 'gives compound status from latest pipelines if ref is nil' do
+ expect(commit.status(nil)).to eq(Ci::Pipeline.latest_status)
+ expect(commit.status(nil)).to eq('failed')
end
end
end
@@ -302,4 +306,21 @@ eos
expect(commit.uri_type('this/path/doesnt/exist')).to be_nil
end
end
+
+ describe '.from_hash' do
+ let(:new_commit) { described_class.from_hash(commit.to_hash, project) }
+
+ it 'returns a Commit' do
+ expect(new_commit).to be_an_instance_of(described_class)
+ end
+
+ it 'wraps a Gitlab::Git::Commit' do
+ expect(new_commit.raw).to be_an_instance_of(Gitlab::Git::Commit)
+ end
+
+ it 'stores the correct commit fields' do
+ expect(new_commit.id).to eq(commit.id)
+ expect(new_commit.message).to eq(commit.message)
+ end
+ end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 80c2a1bc7a9..701f3323c0f 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -175,7 +175,7 @@ describe CommitStatus, models: true do
end
it 'returns statuses without what we want to ignore' do
- is_expected.to eq(statuses.values_at(1, 2, 4, 5, 6, 8, 9))
+ is_expected.to eq(statuses.values_at(0, 1, 2, 3, 4, 5, 6, 8, 9))
end
end
@@ -200,49 +200,6 @@ describe CommitStatus, models: true do
end
end
- describe '#stages' do
- before do
- create :commit_status, pipeline: pipeline, stage: 'build', name: 'linux', stage_idx: 0, status: 'success'
- create :commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'failed'
- create :commit_status, pipeline: pipeline, stage: 'deploy', name: 'staging', stage_idx: 2, status: 'running'
- create :commit_status, pipeline: pipeline, stage: 'test', name: 'rspec', stage_idx: 1, status: 'success'
- end
-
- context 'stages list' do
- subject { CommitStatus.where(pipeline: pipeline).stages }
-
- it 'returns ordered list of stages' do
- is_expected.to eq(%w[build test deploy])
- end
- end
-
- context 'stages with statuses' do
- subject { CommitStatus.where(pipeline: pipeline).latest.stages_status }
-
- it 'returns list of stages with statuses' do
- is_expected.to eq({
- 'build' => 'failed',
- 'test' => 'success',
- 'deploy' => 'running'
- })
- end
-
- context 'when build is retried' do
- before do
- create :commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'success'
- end
-
- it 'ignores a previous state' do
- is_expected.to eq({
- 'build' => 'success',
- 'test' => 'success',
- 'deploy' => 'running'
- })
- end
- end
- end
- end
-
describe '#commit' do
it 'returns commit pipeline has been created for' do
expect(commit_status.commit).to eq project.commit
@@ -277,4 +234,13 @@ describe CommitStatus, models: true do
end
end
end
+
+ describe '#detailed_status' do
+ let(:user) { create(:user) }
+
+ it 'returns a detailed status' do
+ expect(commit_status.detailed_status(user))
+ .to be_a Gitlab::Ci::Status::Success
+ end
+ end
end
diff --git a/spec/models/concerns/access_requestable_spec.rb b/spec/models/concerns/access_requestable_spec.rb
index 96eee0e8bdd..4829ef17a20 100644
--- a/spec/models/concerns/access_requestable_spec.rb
+++ b/spec/models/concerns/access_requestable_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe AccessRequestable do
describe 'Group' do
describe '#request_access' do
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :access_requestable) }
let(:user) { create(:user) }
it { expect(group.request_access(user)).to be_a(GroupMember) }
@@ -11,7 +11,7 @@ describe AccessRequestable do
end
describe '#access_requested?' do
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :access_requestable) }
let(:user) { create(:user) }
before { group.request_access(user) }
@@ -22,14 +22,14 @@ describe AccessRequestable do
describe 'Project' do
describe '#request_access' do
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:user) { create(:user) }
it { expect(project.request_access(user)).to be_a(ProjectMember) }
end
describe '#access_requested?' do
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:user) { create(:user) }
before { project.request_access(user) }
diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb
index 87bffbdc54e..4d0f51fe82a 100644
--- a/spec/models/concerns/has_status_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -48,7 +48,7 @@ describe HasStatus do
[create(type, status: :failed, allow_failure: true)]
end
- it { is_expected.to eq 'success' }
+ it { is_expected.to eq 'skipped' }
end
context 'success and canceled' do
@@ -123,4 +123,100 @@ describe HasStatus do
it_behaves_like 'build status summary'
end
end
+
+ context 'for scope with one status' do
+ shared_examples 'having a job' do |status|
+ %i[ci_build generic_commit_status].each do |type|
+ context "when it's #{status} #{type} job" do
+ let!(:job) { create(type, status) }
+
+ describe ".#{status}" do
+ it 'contains the job' do
+ expect(CommitStatus.public_send(status).all).
+ to contain_exactly(job)
+ end
+ end
+
+ describe '.relevant' do
+ if status == :created
+ it 'contains nothing' do
+ expect(CommitStatus.relevant.all).to be_empty
+ end
+ else
+ it 'contains the job' do
+ expect(CommitStatus.relevant.all).to contain_exactly(job)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ %i[created running pending success
+ failed canceled skipped].each do |status|
+ it_behaves_like 'having a job', status
+ end
+ end
+
+ context 'for scope with more statuses' do
+ shared_examples 'containing the job' do |status|
+ %i[ci_build generic_commit_status].each do |type|
+ context "when it's #{status} #{type} job" do
+ let!(:job) { create(type, status) }
+
+ it 'contains the job' do
+ is_expected.to contain_exactly(job)
+ end
+ end
+ end
+ end
+
+ shared_examples 'not containing the job' do |status|
+ %i[ci_build generic_commit_status].each do |type|
+ context "when it's #{status} #{type} job" do
+ let!(:job) { create(type, status) }
+
+ it 'contains nothing' do
+ is_expected.to be_empty
+ end
+ end
+ end
+ end
+
+ describe '.running_or_pending' do
+ subject { CommitStatus.running_or_pending }
+
+ %i[running pending].each do |status|
+ it_behaves_like 'containing the job', status
+ end
+
+ %i[created failed success].each do |status|
+ it_behaves_like 'not containing the job', status
+ end
+ end
+
+ describe '.finished' do
+ subject { CommitStatus.finished }
+
+ %i[success failed canceled].each do |status|
+ it_behaves_like 'containing the job', status
+ end
+
+ %i[created running pending].each do |status|
+ it_behaves_like 'not containing the job', status
+ end
+ end
+
+ describe '.cancelable' do
+ subject { CommitStatus.cancelable }
+
+ %i[running pending created].each do |status|
+ it_behaves_like 'containing the job', status
+ end
+
+ %i[failed success skipped canceled].each do |status|
+ it_behaves_like 'not containing the job', status
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index a9603074c32..4fa06a8c60a 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -35,7 +35,7 @@ describe Issue, "Issuable" do
it { is_expected.to validate_presence_of(:iid) }
it { is_expected.to validate_presence_of(:author) }
it { is_expected.to validate_presence_of(:title) }
- it { is_expected.to validate_length_of(:title).is_at_least(0).is_at_most(255) }
+ it { is_expected.to validate_length_of(:title).is_at_most(255) }
end
describe "Scope" do
@@ -97,6 +97,11 @@ describe Issue, "Issuable" do
end
end
+ describe '.to_ability_name' do
+ it { expect(Issue.to_ability_name).to eq("issue") }
+ it { expect(MergeRequest.to_ability_name).to eq("merge_request") }
+ end
+
describe "#today?" do
it "returns true when created today" do
# Avoid timezone differences and just return exactly what we want
@@ -171,23 +176,25 @@ describe Issue, "Issuable" do
end
describe '#subscribed?' do
+ let(:project) { issue.project }
+
context 'user is not a participant in the issue' do
before { allow(issue).to receive(:participants).with(user).and_return([]) }
it 'returns false when no subcription exists' do
- expect(issue.subscribed?(user)).to be_falsey
+ expect(issue.subscribed?(user, project)).to be_falsey
end
it 'returns true when a subcription exists and subscribed is true' do
- issue.subscriptions.create(user: user, subscribed: true)
+ issue.subscriptions.create(user: user, project: project, subscribed: true)
- expect(issue.subscribed?(user)).to be_truthy
+ expect(issue.subscribed?(user, project)).to be_truthy
end
it 'returns false when a subcription exists and subscribed is false' do
- issue.subscriptions.create(user: user, subscribed: false)
+ issue.subscriptions.create(user: user, project: project, subscribed: false)
- expect(issue.subscribed?(user)).to be_falsey
+ expect(issue.subscribed?(user, project)).to be_falsey
end
end
@@ -195,19 +202,19 @@ describe Issue, "Issuable" do
before { allow(issue).to receive(:participants).with(user).and_return([user]) }
it 'returns false when no subcription exists' do
- expect(issue.subscribed?(user)).to be_truthy
+ expect(issue.subscribed?(user, project)).to be_truthy
end
it 'returns true when a subcription exists and subscribed is true' do
- issue.subscriptions.create(user: user, subscribed: true)
+ issue.subscriptions.create(user: user, project: project, subscribed: true)
- expect(issue.subscribed?(user)).to be_truthy
+ expect(issue.subscribed?(user, project)).to be_truthy
end
it 'returns false when a subcription exists and subscribed is false' do
- issue.subscriptions.create(user: user, subscribed: false)
+ issue.subscriptions.create(user: user, project: project, subscribed: false)
- expect(issue.subscribed?(user)).to be_falsey
+ expect(issue.subscribed?(user, project)).to be_falsey
end
end
end
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index b7e973798a3..0e097559b59 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -115,4 +115,24 @@ describe Milestone, 'Milestoneish' do
expect(milestone.percent_complete(admin)).to eq 60
end
end
+
+ describe '#elapsed_days' do
+ it 'shows 0 if no start_date set' do
+ milestone = build(:milestone)
+
+ expect(milestone.elapsed_days).to eq(0)
+ end
+
+ it 'shows 0 if start_date is a future' do
+ milestone = build(:milestone, start_date: Time.now + 2.days)
+
+ expect(milestone.elapsed_days).to eq(0)
+ end
+
+ it 'shows correct amount of days' do
+ milestone = build(:milestone, start_date: Time.now - 2.days)
+
+ expect(milestone.elapsed_days).to eq(2)
+ end
+ end
end
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
new file mode 100644
index 00000000000..b556135532f
--- /dev/null
+++ b/spec/models/concerns/routable_spec.rb
@@ -0,0 +1,71 @@
+require 'spec_helper'
+
+describe Group, 'Routable' do
+ let!(:group) { create(:group) }
+
+ describe 'Validations' do
+ it { is_expected.to validate_presence_of(:route) }
+ end
+
+ describe 'Associations' do
+ it { is_expected.to have_one(:route).dependent(:destroy) }
+ end
+
+ describe 'Callbacks' do
+ it 'creates route record on create' do
+ expect(group.route.path).to eq(group.path)
+ end
+
+ it 'updates route record on path change' do
+ group.update_attributes(path: 'wow')
+
+ expect(group.route.path).to eq('wow')
+ end
+
+ it 'ensure route path uniqueness across different objects' do
+ create(:group, parent: group, path: 'xyz')
+ duplicate = build(:project, namespace: group, path: 'xyz')
+
+ expect { duplicate.save! }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Route path has already been taken, Route is invalid')
+ end
+ end
+
+ describe '.find_by_full_path' do
+ let!(:nested_group) { create(:group, parent: group) }
+
+ it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) }
+ it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) }
+ it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) }
+ it { expect(described_class.find_by_full_path('unknown')).to eq(nil) }
+ end
+
+ describe '.where_full_path_in' do
+ context 'without any paths' do
+ it 'returns an empty relation' do
+ expect(described_class.where_full_path_in([])).to eq([])
+ end
+ end
+
+ context 'without any valid paths' do
+ it 'returns an empty relation' do
+ expect(described_class.where_full_path_in(%w[unknown])).to eq([])
+ end
+ end
+
+ context 'with valid paths' do
+ let!(:nested_group) { create(:group, parent: group) }
+
+ it 'returns the projects matching the paths' do
+ result = described_class.where_full_path_in([group.to_param, nested_group.to_param])
+
+ expect(result).to contain_exactly(group, nested_group)
+ end
+
+ it 'returns projects regardless of the casing of paths' do
+ result = described_class.where_full_path_in([group.to_param.upcase, nested_group.to_param.upcase])
+
+ expect(result).to contain_exactly(group, nested_group)
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/subscribable_spec.rb b/spec/models/concerns/subscribable_spec.rb
index b7fc5a92497..58f5c164116 100644
--- a/spec/models/concerns/subscribable_spec.rb
+++ b/spec/models/concerns/subscribable_spec.rb
@@ -1,67 +1,128 @@
require 'spec_helper'
describe Subscribable, 'Subscribable' do
- let(:resource) { create(:issue) }
- let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:resource) { create(:issue, project: project) }
+ let(:user_1) { create(:user) }
describe '#subscribed?' do
- it 'returns false when no subcription exists' do
- expect(resource.subscribed?(user)).to be_falsey
- end
+ context 'without project' do
+ it 'returns false when no subscription exists' do
+ expect(resource.subscribed?(user_1)).to be_falsey
+ end
+
+ it 'returns true when a subcription exists and subscribed is true' do
+ resource.subscriptions.create(user: user_1, subscribed: true)
+
+ expect(resource.subscribed?(user_1)).to be_truthy
+ end
- it 'returns true when a subcription exists and subscribed is true' do
- resource.subscriptions.create(user: user, subscribed: true)
+ it 'returns false when a subcription exists and subscribed is false' do
+ resource.subscriptions.create(user: user_1, subscribed: false)
- expect(resource.subscribed?(user)).to be_truthy
+ expect(resource.subscribed?(user_1)).to be_falsey
+ end
end
- it 'returns false when a subcription exists and subscribed is false' do
- resource.subscriptions.create(user: user, subscribed: false)
+ context 'with project' do
+ it 'returns false when no subscription exists' do
+ expect(resource.subscribed?(user_1, project)).to be_falsey
+ end
+
+ it 'returns true when a subcription exists and subscribed is true' do
+ resource.subscriptions.create(user: user_1, project: project, subscribed: true)
+
+ expect(resource.subscribed?(user_1, project)).to be_truthy
+ end
- expect(resource.subscribed?(user)).to be_falsey
+ it 'returns false when a subcription exists and subscribed is false' do
+ resource.subscriptions.create(user: user_1, project: project, subscribed: false)
+
+ expect(resource.subscribed?(user_1, project)).to be_falsey
+ end
end
end
+
describe '#subscribers' do
it 'returns [] when no subcribers exists' do
- expect(resource.subscribers).to be_empty
+ expect(resource.subscribers(project)).to be_empty
end
it 'returns the subscribed users' do
- resource.subscriptions.create(user: user, subscribed: true)
- resource.subscriptions.create(user: create(:user), subscribed: false)
+ user_2 = create(:user)
+ resource.subscriptions.create(user: user_1, subscribed: true)
+ resource.subscriptions.create(user: user_2, project: project, subscribed: true)
+ resource.subscriptions.create(user: create(:user), project: project, subscribed: false)
- expect(resource.subscribers).to eq [user]
+ expect(resource.subscribers(project)).to contain_exactly(user_1, user_2)
end
end
describe '#toggle_subscription' do
- it 'toggles the current subscription state for the given user' do
- expect(resource.subscribed?(user)).to be_falsey
+ context 'without project' do
+ it 'toggles the current subscription state for the given user' do
+ expect(resource.subscribed?(user_1)).to be_falsey
- resource.toggle_subscription(user)
+ resource.toggle_subscription(user_1)
- expect(resource.subscribed?(user)).to be_truthy
+ expect(resource.subscribed?(user_1)).to be_truthy
+ end
+ end
+
+ context 'with project' do
+ it 'toggles the current subscription state for the given user' do
+ expect(resource.subscribed?(user_1, project)).to be_falsey
+
+ resource.toggle_subscription(user_1, project)
+
+ expect(resource.subscribed?(user_1, project)).to be_truthy
+ end
end
end
describe '#subscribe' do
- it 'subscribes the given user' do
- expect(resource.subscribed?(user)).to be_falsey
+ context 'without project' do
+ it 'subscribes the given user' do
+ expect(resource.subscribed?(user_1)).to be_falsey
+
+ resource.subscribe(user_1)
+
+ expect(resource.subscribed?(user_1)).to be_truthy
+ end
+ end
+
+ context 'with project' do
+ it 'subscribes the given user' do
+ expect(resource.subscribed?(user_1, project)).to be_falsey
- resource.subscribe(user)
+ resource.subscribe(user_1, project)
- expect(resource.subscribed?(user)).to be_truthy
+ expect(resource.subscribed?(user_1, project)).to be_truthy
+ end
end
end
describe '#unsubscribe' do
- it 'unsubscribes the given current user' do
- resource.subscriptions.create(user: user, subscribed: true)
- expect(resource.subscribed?(user)).to be_truthy
+ context 'without project' do
+ it 'unsubscribes the given current user' do
+ resource.subscriptions.create(user: user_1, subscribed: true)
+ expect(resource.subscribed?(user_1)).to be_truthy
+
+ resource.unsubscribe(user_1)
+
+ expect(resource.subscribed?(user_1)).to be_falsey
+ end
+ end
+
+ context 'with project' do
+ it 'unsubscribes the given current user' do
+ resource.subscriptions.create(user: user_1, project: project, subscribed: true)
+ expect(resource.subscribed?(user_1, project)).to be_truthy
- resource.unsubscribe(user)
+ resource.unsubscribe(user_1, project)
- expect(resource.subscribed?(user)).to be_falsey
+ expect(resource.subscribed?(user_1, project)).to be_falsey
+ end
end
end
end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index eb64f3d0c83..4b0bfa43abf 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -6,6 +6,7 @@ shared_examples 'TokenAuthenticatable' do
it { expect(described_class).to be_private_method_defined(:write_new_token) }
it { expect(described_class).to respond_to("find_by_#{token_field}") }
it { is_expected.to respond_to("ensure_#{token_field}") }
+ it { is_expected.to respond_to("set_#{token_field}") }
it { is_expected.to respond_to("reset_#{token_field}!") }
end
end
@@ -55,6 +56,12 @@ describe ApplicationSetting, 'TokenAuthenticatable' do
end
end
+ describe 'setting new token' do
+ subject { described_class.new.send("set_#{token_field}", '0123456789') }
+
+ it { is_expected.to eq '0123456789' }
+ end
+
describe 'multiple token fields' do
before do
described_class.send(:add_authentication_token_field, :yet_another_token)
diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb
index 7691d690db0..7771785ead3 100644
--- a/spec/models/cycle_analytics/code_spec.rb
+++ b/spec/models/cycle_analytics/code_spec.rb
@@ -6,7 +6,7 @@ describe 'CycleAnalytics#code', feature: true do
let(:project) { create(:project) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, from: from_date) }
+ subject { CycleAnalytics.new(project, user, from: from_date) }
context 'with deployment' do
generate_cycle_analytics_spec(
diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb
index f649b44d367..5ed3d37f2fb 100644
--- a/spec/models/cycle_analytics/issue_spec.rb
+++ b/spec/models/cycle_analytics/issue_spec.rb
@@ -6,7 +6,7 @@ describe 'CycleAnalytics#issue', models: true do
let(:project) { create(:project) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, from: from_date) }
+ subject { CycleAnalytics.new(project, user, from: from_date) }
generate_cycle_analytics_spec(
phase: :issue,
diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb
index 2cdefbeef21..baf3e3241a1 100644
--- a/spec/models/cycle_analytics/plan_spec.rb
+++ b/spec/models/cycle_analytics/plan_spec.rb
@@ -6,7 +6,7 @@ describe 'CycleAnalytics#plan', feature: true do
let(:project) { create(:project) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, from: from_date) }
+ subject { CycleAnalytics.new(project, user, from: from_date) }
generate_cycle_analytics_spec(
phase: :plan,
diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb
index 1f5e5cab92d..21b9c6e7150 100644
--- a/spec/models/cycle_analytics/production_spec.rb
+++ b/spec/models/cycle_analytics/production_spec.rb
@@ -6,7 +6,7 @@ describe 'CycleAnalytics#production', feature: true do
let(:project) { create(:project) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, from: from_date) }
+ subject { CycleAnalytics.new(project, user, from: from_date) }
generate_cycle_analytics_spec(
phase: :production,
diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb
index 0ed080a42b1..158621d59a4 100644
--- a/spec/models/cycle_analytics/review_spec.rb
+++ b/spec/models/cycle_analytics/review_spec.rb
@@ -6,7 +6,7 @@ describe 'CycleAnalytics#review', feature: true do
let(:project) { create(:project) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, from: from_date) }
+ subject { CycleAnalytics.new(project, user, from: from_date) }
generate_cycle_analytics_spec(
phase: :review,
diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb
index af1c4477ddb..dad653964b7 100644
--- a/spec/models/cycle_analytics/staging_spec.rb
+++ b/spec/models/cycle_analytics/staging_spec.rb
@@ -6,7 +6,7 @@ describe 'CycleAnalytics#staging', feature: true do
let(:project) { create(:project) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, from: from_date) }
+ subject { CycleAnalytics.new(project, user, from: from_date) }
generate_cycle_analytics_spec(
phase: :staging,
diff --git a/spec/models/cycle_analytics/summary_spec.rb b/spec/models/cycle_analytics/summary_spec.rb
index 9d67bc82cba..725bc68b25f 100644
--- a/spec/models/cycle_analytics/summary_spec.rb
+++ b/spec/models/cycle_analytics/summary_spec.rb
@@ -4,7 +4,7 @@ describe CycleAnalytics::Summary, models: true do
let(:project) { create(:project) }
let(:from) { Time.now }
let(:user) { create(:user, :admin) }
- subject { described_class.new(project, from: from) }
+ subject { described_class.new(project, user, from: from) }
describe "#new_issues" do
it "finds the number of issues created after the 'from date'" do
diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb
index 02ddfeed9c1..2313724e8f3 100644
--- a/spec/models/cycle_analytics/test_spec.rb
+++ b/spec/models/cycle_analytics/test_spec.rb
@@ -6,7 +6,7 @@ describe 'CycleAnalytics#test', feature: true do
let(:project) { create(:project) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, from: from_date) }
+ subject { CycleAnalytics.new(project, user, from: from_date) }
generate_cycle_analytics_spec(
phase: :test,
diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb
index 0142706d140..bc32fadd391 100644
--- a/spec/models/discussion_spec.rb
+++ b/spec/models/discussion_spec.rb
@@ -521,6 +521,15 @@ describe Discussion, model: true do
end
end
+ describe "#first_note_to_resolve" do
+ it "returns the first not that still needs to be resolved" do
+ allow(first_note).to receive(:to_be_resolved?).and_return(false)
+ allow(second_note).to receive(:to_be_resolved?).and_return(true)
+
+ expect(subject.first_note_to_resolve).to eq(second_note)
+ end
+ end
+
describe "#collapsed?" do
context "when a diff discussion" do
before do
@@ -590,4 +599,23 @@ describe Discussion, model: true do
end
end
end
+
+ describe "#truncated_diff_lines" do
+ let(:truncated_lines) { subject.truncated_diff_lines }
+
+ context "when diff is greater than allowed number of truncated diff lines " do
+ it "returns fewer lines" do
+ expect(subject.diff_lines.count).to be > described_class::NUMBER_OF_TRUNCATED_DIFF_LINES
+
+ expect(truncated_lines.count).to be <= described_class::NUMBER_OF_TRUNCATED_DIFF_LINES
+ end
+ end
+
+ context "when some diff lines are meta" do
+ it "returns no meta lines" do
+ expect(subject.diff_lines).to include(be_meta)
+ expect(truncated_lines).not_to include(be_meta)
+ end
+ end
+ end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index a94e6d0165f..97cbb093ed2 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Environment, models: true do
- let(:environment) { create(:environment) }
+ subject(:environment) { create(:environment) }
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:deployments) }
@@ -9,20 +9,17 @@ describe Environment, models: true do
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) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
- it { is_expected.to validate_length_of(:name).is_within(0..255) }
+ it { is_expected.to validate_length_of(:name).is_at_most(255) }
- it { is_expected.to validate_length_of(:external_url).is_within(0..255) }
+ it { is_expected.to validate_uniqueness_of(:slug).scoped_to(:project_id) }
+ it { is_expected.to validate_length_of(:slug).is_at_most(24) }
- # To circumvent a not null violation of the name column:
- # https://github.com/thoughtbot/shoulda-matchers/issues/336
- it 'validates uniqueness of :external_url' do
- create(:environment)
-
- is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id)
- end
+ 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 '#nullify_external_url' do
it 'replaces a blank url with nil' do
@@ -166,4 +163,70 @@ describe Environment, models: true do
end
end
end
+
+ describe 'recently_updated_on_branch?' do
+ subject { environment.recently_updated_on_branch?('feature') }
+
+ context 'when last deployment to environment is the most recent one' do
+ before do
+ create(:deployment, environment: environment, ref: 'feature')
+ end
+
+ it { is_expected.to be true }
+ end
+
+ context 'when last deployment to environment is not the most recent' do
+ before do
+ create(:deployment, environment: environment, ref: 'feature')
+ create(:deployment, environment: environment, ref: 'master')
+ end
+
+ it { is_expected.to be false }
+ end
+ end
+
+ describe '#actions_for' do
+ let(:deployment) { create(:deployment, environment: environment) }
+ let(:pipeline) { deployment.deployable.pipeline }
+ let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_BUILD_REF_NAME' )}
+ let!(:production_action) { create(:ci_build, :manual, name: 'production', pipeline: pipeline, environment: 'production' )}
+
+ it 'returns a list of actions with matching environment' do
+ expect(environment.actions_for('review/master')).to contain_exactly(review_action)
+ end
+ end
+
+ describe '#slug' do
+ it "is automatically generated" do
+ expect(environment.slug).not_to be_nil
+ end
+
+ it "is not regenerated if name changes" do
+ original_slug = environment.slug
+ environment.update_attributes!(name: environment.name.reverse)
+
+ expect(environment.slug).to eq(original_slug)
+ end
+ end
+
+ describe '#generate_slug' do
+ SUFFIX = "-[a-z0-9]{6}"
+ {
+ "staging-12345678901234567" => "staging-123456789" + SUFFIX,
+ "9-staging-123456789012345" => "env-9-staging-123" + SUFFIX,
+ "staging-1234567890123456" => "staging-1234567890123456",
+ "production" => "production",
+ "PRODUCTION" => "production" + SUFFIX,
+ "review/1-foo" => "review-1-foo" + SUFFIX,
+ "1-foo" => "env-1-foo" + SUFFIX,
+ "1/foo" => "env-1-foo" + SUFFIX,
+ "foo-" => "foo" + SUFFIX,
+ }.each do |name, matcher|
+ it "returns a slug matching #{matcher}, given #{name}" do
+ slug = described_class.new(name: name).generate_slug
+
+ expect(slug).to match(/\A#{matcher}\z/)
+ end
+ end
+ end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index aca49be2942..f8660da031d 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -27,13 +27,14 @@ describe Event, models: true do
end
describe "Push event" do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :private) }
let(:user) { project.owner }
let(:event) { create_event(project, user) }
it do
expect(event.push?).to be_truthy
- expect(event.visible_to_user?).to be_truthy
+ expect(event.visible_to_user?(user)).to be_truthy
+ expect(event.visible_to_user?(nil)).to be_falsey
expect(event.tag?).to be_falsey
expect(event.branch_name).to eq("master")
expect(event.author).to eq(user)
@@ -93,6 +94,7 @@ describe Event, models: true do
let(:admin) { create(:admin) }
let(:issue) { create(:issue, project: project, author: author, assignee: assignee) }
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
+ let(:note_on_commit) { create(:note_on_commit, project: project) }
let(:note_on_issue) { create(:note_on_issue, noteable: issue, project: project) }
let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project) }
let(:event) { Event.new(project: project, target: target, author_id: author.id) }
@@ -102,6 +104,32 @@ describe Event, models: true do
project.team << [guest, :guest]
end
+ context 'commit note event' do
+ let(:target) { note_on_commit }
+
+ it do
+ aggregate_failures do
+ expect(event.visible_to_user?(non_member)).to eq true
+ expect(event.visible_to_user?(member)).to eq true
+ expect(event.visible_to_user?(guest)).to eq true
+ expect(event.visible_to_user?(admin)).to eq true
+ end
+ end
+
+ context 'private project' do
+ let(:project) { create(:empty_project, :private) }
+
+ it do
+ aggregate_failures do
+ expect(event.visible_to_user?(non_member)).to eq false
+ expect(event.visible_to_user?(member)).to eq true
+ expect(event.visible_to_user?(guest)).to eq false
+ expect(event.visible_to_user?(admin)).to eq true
+ end
+ end
+ end
+ end
+
context 'issue event' do
context 'for non confidential issues' do
let(:target) { issue }
@@ -232,6 +260,24 @@ describe Event, models: true do
end
end
+ describe '#authored_by?' do
+ let(:event) { build(:event) }
+
+ it 'returns true when the event author and user are the same' do
+ expect(event.authored_by?(event.author)).to eq(true)
+ end
+
+ it 'returns false when passing nil as an argument' do
+ expect(event.authored_by?(nil)).to eq(false)
+ end
+
+ it 'returns false when the given user is not the author of the event' do
+ user = double(:user, id: -1)
+
+ expect(event.authored_by?(user)).to eq(false)
+ end
+ end
+
def create_event(project, user, attrs = {})
data = {
before: Gitlab::Git::BLANK_SHA,
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index 615cfe3142b..6004bfdb7b7 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -1,8 +1,11 @@
require 'spec_helper'
describe GenericCommitStatus, models: true do
- let(:pipeline) { FactoryGirl.create :ci_pipeline }
- let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, pipeline: pipeline }
+ let(:pipeline) { create(:ci_pipeline) }
+
+ let(:generic_commit_status) do
+ create(:generic_commit_status, pipeline: pipeline)
+ end
describe '#context' do
subject { generic_commit_status.context }
@@ -17,6 +20,15 @@ describe GenericCommitStatus, models: true do
it { is_expected.to eq([:external]) }
end
+ describe '#detailed_status' do
+ let(:user) { create(:user) }
+
+ it 'returns detailed status object' do
+ expect(generic_commit_status.detailed_status(user))
+ .to be_a Gitlab::Ci::Status::Success
+ end
+ end
+
describe 'set_default_values' do
before do
generic_commit_status.context = nil
diff --git a/spec/models/group_label_spec.rb b/spec/models/group_label_spec.rb
index 85eb889225b..668aa6fb357 100644
--- a/spec/models/group_label_spec.rb
+++ b/spec/models/group_label_spec.rb
@@ -18,7 +18,7 @@ describe GroupLabel, models: true do
end
describe '#to_reference' do
- let(:label) { create(:group_label) }
+ let(:label) { create(:group_label, title: 'feature') }
context 'using id' do
it 'returns a String reference to the object' do
@@ -37,6 +37,16 @@ describe GroupLabel, models: true do
end
end
+ context 'cross-project' do
+ let(:namespace) { build_stubbed(:namespace) }
+ let(:source_project) { build_stubbed(:empty_project, name: 'project-1', namespace: namespace) }
+ let(:target_project) { build_stubbed(:empty_project, name: 'project-2', namespace: namespace) }
+
+ it 'returns a String reference to the object' do
+ expect(label.to_reference(source_project, target_project)).to eq %(project-1~#{label.id})
+ end
+ end
+
context 'using invalid format' do
it 'raises error' do
expect { label.to_reference(format: :invalid) }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 47f89f744cb..7d5ecfbaa64 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Group, models: true do
- let!(:group) { create(:group) }
+ let!(:group) { create(:group, :access_requestable) }
describe 'associations' do
it { is_expected.to have_many :projects }
@@ -50,9 +50,8 @@ describe Group, models: true do
describe 'validations' do
it { is_expected.to validate_presence_of :name }
- it { is_expected.to validate_uniqueness_of(:name) }
+ it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) }
it { is_expected.to validate_presence_of :path }
- it { is_expected.to validate_uniqueness_of(:path) }
it { is_expected.not_to validate_presence_of :owner }
end
@@ -271,4 +270,11 @@ describe Group, models: true do
expect(group.web_url).to include("groups/#{group.name}")
end
end
+
+ describe 'nested group' do
+ subject { build(:group, :nested) }
+
+ it { is_expected.to be_valid }
+ it { expect(subject.parent).to be_kind_of(Group) }
+ end
end
diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb
new file mode 100644
index 00000000000..d79f929f7a1
--- /dev/null
+++ b/spec/models/guest_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Guest, lib: true do
+ let(:public_project) { create(:project, :public) }
+ let(:private_project) { create(:project, :private) }
+ let(:internal_project) { create(:project, :internal) }
+
+ describe '.can_pull?' do
+ context 'when project is private' do
+ it 'does not allow to pull the repo' do
+ expect(Guest.can?(:download_code, private_project)).to eq(false)
+ end
+ end
+
+ context 'when project is internal' do
+ it 'does not allow to pull the repo' do
+ expect(Guest.can?(:download_code, internal_project)).to eq(false)
+ end
+ end
+
+ context 'when project is public' do
+ context 'when repository is disabled' do
+ it 'does not allow to pull the repo' do
+ public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
+
+ expect(Guest.can?(:download_code, public_project)).to eq(false)
+ end
+ end
+
+ context 'when repository is accessible only by team members' do
+ it 'does not allow to pull the repo' do
+ public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::PRIVATE)
+
+ expect(Guest.can?(:download_code, public_project)).to eq(false)
+ end
+ end
+
+ context 'when repository is enabled' do
+ it 'allows to pull the repo' do
+ public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::ENABLED)
+
+ expect(Guest.can?(:download_code, public_project)).to eq(true)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 60d30eb7418..bb56e44db29 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -22,35 +22,17 @@ describe Issue, models: true do
it { is_expected.to have_db_index(:deleted_at) }
end
- describe 'visible_to_user' do
- let(:user) { create(:user) }
- let(:authorized_user) { create(:user) }
- let(:project) { create(:project, namespace: authorized_user.namespace) }
- let!(:public_issue) { create(:issue, project: project) }
- let!(:confidential_issue) { create(:issue, project: project, confidential: true) }
-
- it 'returns non confidential issues for nil user' do
- expect(Issue.visible_to_user(nil).count).to be(1)
- end
-
- it 'returns non confidential issues for user not authorized for the issues projects' do
- expect(Issue.visible_to_user(user).count).to be(1)
- end
-
- it 'returns all issues for user authorized for the issues projects' do
- expect(Issue.visible_to_user(authorized_user).count).to be(2)
- end
- end
-
describe '#to_reference' do
+ let(:project) { build(:empty_project, name: 'sample-project') }
+ let(:issue) { build(:issue, iid: 1, project: project) }
+
it 'returns a String reference to the object' do
- expect(subject.to_reference).to eq "##{subject.iid}"
+ expect(issue.to_reference).to eq "#1"
end
it 'supports a cross-project reference' do
- cross = double('project')
- expect(subject.to_reference(cross)).
- to eq "#{subject.project.to_reference}##{subject.iid}"
+ another_project = build(:project, name: 'another-project', namespace: project.namespace)
+ expect(issue.to_reference(another_project)).to eq "sample-project#1"
end
end
@@ -102,17 +84,17 @@ describe Issue, models: true do
it 'returns the merge request to close this issue' do
mr
- expect(issue.closed_by_merge_requests).to eq([mr])
+ expect(issue.closed_by_merge_requests(mr.author)).to eq([mr])
end
it "returns an empty array when the merge request is closed already" do
closed_mr
- expect(issue.closed_by_merge_requests).to eq([])
+ expect(issue.closed_by_merge_requests(closed_mr.author)).to eq([])
end
it "returns an empty array when the current issue is closed already" do
- expect(closed_issue.closed_by_merge_requests).to eq([])
+ expect(closed_issue.closed_by_merge_requests(closed_issue.author)).to eq([])
end
end
@@ -218,7 +200,7 @@ describe Issue, models: true do
source_project: subject.project,
source_branch: "#{subject.iid}-branch" })
merge_request.create_cross_references!(user)
- expect(subject.referenced_merge_requests).not_to be_empty
+ expect(subject.referenced_merge_requests(user)).not_to be_empty
expect(subject.related_branches(user)).to eq([subject.to_branch_name])
end
@@ -314,8 +296,24 @@ describe Issue, models: true do
end
describe '#visible_to_user?' do
+ context 'without a user' do
+ let(:issue) { build(:issue) }
+
+ it 'returns true when the issue is publicly visible' do
+ expect(issue).to receive(:publicly_visible?).and_return(true)
+
+ expect(issue.visible_to_user?).to eq(true)
+ end
+
+ it 'returns false when the issue is not publicly visible' do
+ expect(issue).to receive(:publicly_visible?).and_return(false)
+
+ expect(issue.visible_to_user?).to eq(false)
+ end
+ end
+
context 'with a user' do
- let(:user) { build(:user) }
+ let(:user) { create(:user) }
let(:issue) { build(:issue) }
it 'returns true when the issue is readable' do
@@ -329,26 +327,24 @@ describe Issue, models: true do
expect(issue.visible_to_user?(user)).to eq(false)
end
- end
- context 'without a user' do
- let(:issue) { build(:issue) }
+ it 'returns false when feature is disabled' do
+ expect(issue).not_to receive(:readable_by?)
- it 'returns true when the issue is publicly visible' do
- expect(issue).to receive(:publicly_visible?).and_return(true)
+ issue.project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
- expect(issue.visible_to_user?).to eq(true)
+ expect(issue.visible_to_user?(user)).to eq(false)
end
- it 'returns false when the issue is not publicly visible' do
- expect(issue).to receive(:publicly_visible?).and_return(false)
+ it 'returns false when restricted for members' do
+ expect(issue).not_to receive(:readable_by?)
- expect(issue.visible_to_user?).to eq(false)
+ issue.project.project_feature.update_attribute(:issues_access_level, ProjectFeature::PRIVATE)
+
+ expect(issue.visible_to_user?(user)).to eq(false)
end
end
- end
- describe '#readable_by?' do
describe 'with a regular user that is not a team member' do
let(:user) { create(:user) }
@@ -358,13 +354,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns false for a confidential issue' do
issue = build(:issue, project: project, confidential: true)
- expect(issue).not_to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(false)
end
end
@@ -375,13 +371,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).not_to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(false)
end
end
@@ -393,13 +389,13 @@ describe Issue, models: true do
it 'returns false for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).not_to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(false)
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).not_to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(false)
end
end
end
@@ -410,26 +406,28 @@ describe Issue, models: true do
it 'returns false for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).not_to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(false)
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).not_to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(false)
end
context 'when the user is the project owner' do
+ before { project.team << [user, :master] }
+
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).not_to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns true for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).not_to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
end
end
@@ -447,13 +445,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns true for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
end
@@ -467,13 +465,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns true for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
end
@@ -487,13 +485,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns true for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
end
end
@@ -505,13 +503,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns true for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
end
end
@@ -523,13 +521,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).to be_publicly_visible
+ expect(issue).to be_truthy
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).not_to be_publicly_visible
+ expect(issue).not_to be_falsy
end
end
@@ -539,13 +537,13 @@ describe Issue, models: true do
it 'returns false for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).not_to be_publicly_visible
+ expect(issue).not_to be_falsy
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).not_to be_publicly_visible
+ expect(issue).not_to be_falsy
end
end
@@ -555,13 +553,13 @@ describe Issue, models: true do
it 'returns false for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).not_to be_publicly_visible
+ expect(issue).not_to be_falsy
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).not_to be_publicly_visible
+ expect(issue).not_to be_falsy
end
end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 7fc6ed1dd54..2a33d819138 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -7,9 +7,13 @@ describe Key, models: true do
describe "Validation" do
it { is_expected.to validate_presence_of(:title) }
+ it { is_expected.to validate_length_of(:title).is_at_most(255) }
+
it { is_expected.to validate_presence_of(:key) }
- it { is_expected.to validate_length_of(:title).is_within(0..255) }
- it { is_expected.to validate_length_of(:key).is_within(0..5000) }
+ it { is_expected.to validate_length_of(:key).is_at_most(5000) }
+ it { is_expected.to allow_value('ssh-foo').for(:key) }
+ it { is_expected.to allow_value('ecdsa-foo').for(:key) }
+ it { is_expected.not_to allow_value('foo-bar').for(:key) }
end
describe "Methods" do
@@ -19,7 +23,7 @@ describe Key, models: true do
describe "#publishable_keys" do
it 'replaces SSH key comment with simple identifier of username + hostname' do
- expect(build(:key, user: user).publishable_key).to include("#{user.name} (localhost)")
+ expect(build(:key, user: user).publishable_key).to include("#{user.name} (#{Gitlab.config.gitlab.host})")
end
end
end
@@ -71,15 +75,25 @@ describe Key, models: true do
context 'callbacks' do
it 'adds new key to authorized_file' do
- @key = build(:personal_key, id: 7)
- expect(GitlabShellWorker).to receive(:perform_async).with(:add_key, @key.shell_id, @key.key)
- @key.save
+ key = build(:personal_key, id: 7)
+ expect(GitlabShellWorker).to receive(:perform_async).with(:add_key, key.shell_id, key.key)
+ key.save!
end
it 'removes key from authorized_file' do
- @key = create(:personal_key)
- expect(GitlabShellWorker).to receive(:perform_async).with(:remove_key, @key.shell_id, @key.key)
- @key.destroy
+ key = create(:personal_key)
+ expect(GitlabShellWorker).to receive(:perform_async).with(:remove_key, key.shell_id, key.key)
+ key.destroy
+ end
+ end
+
+ describe '#key=' do
+ let(:valid_key) do
+ "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0= dummy@gitlab.com"
+ end
+
+ it 'strips white spaces' do
+ expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key)
end
end
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 485121701af..4f7c8a36cb5 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -57,7 +57,7 @@ describe Member, models: true do
describe 'Scopes & finders' do
before do
- project = create(:empty_project, :public)
+ project = create(:empty_project, :public, :access_requestable)
group = create(:group)
@owner_user = create(:user).tap { |u| group.add_owner(u) }
@owner = group.members.find_by(user_id: @owner_user.id)
@@ -174,7 +174,7 @@ describe Member, models: true do
describe '.add_user' do
%w[project group].each do |source_type|
context "when source is a #{source_type}" do
- let!(:source) { create(source_type, :public) }
+ let!(:source) { create(source_type, :public, :access_requestable) }
let!(:user) { create(:user) }
let!(:admin) { create(:admin) }
@@ -443,6 +443,16 @@ describe Member, models: true do
member.accept_invite!(user)
end
+
+ it "refreshes user's authorized projects", truncate: true do
+ project = member.source
+
+ expect(user.authorized_projects).not_to include(project)
+
+ member.accept_invite!(user)
+
+ expect(user.authorized_projects.reload).to include(project)
+ end
end
describe "#decline_invite!" do
@@ -468,4 +478,16 @@ describe Member, models: true do
expect { member.generate_invite_token }.to change { member.invite_token}
end
end
+
+ describe "destroying a record", truncate: true do
+ it "refreshes user's authorized projects" do
+ project = create(:project, :private)
+ user = create(:user)
+ member = project.team << [user, :reporter]
+
+ member.destroy
+
+ expect(user.authorized_projects).not_to include(project)
+ end
+ end
end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index e5007424041..eb876d105da 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -77,24 +77,13 @@ describe MergeRequestDiff, models: true do
end
describe '#commits_sha' do
- shared_examples 'returning all commits SHA' do
- it 'returns all commits SHA' do
- commits_sha = subject.commits_sha
+ it 'returns all commits SHA using serialized commits' do
+ subject.st_commits = [
+ { id: 'sha1' },
+ { id: 'sha2' }
+ ]
- expect(commits_sha).to eq(subject.commits.map(&:sha))
- end
- end
-
- context 'when commits were loaded' do
- before do
- subject.commits
- end
-
- it_behaves_like 'returning all commits SHA'
- end
-
- context 'when commits were not loaded' do
- it_behaves_like 'returning all commits SHA'
+ expect(subject.commits_sha).to eq(['sha1', 'sha2'])
end
end
@@ -113,4 +102,15 @@ describe MergeRequestDiff, models: true do
expect(diffs.size).to eq(3)
end
end
+
+ describe '#commits_count' do
+ it 'returns number of commits using serialized commits' do
+ subject.st_commits = [
+ { id: 'sha1' },
+ { id: 'sha2' }
+ ]
+
+ expect(subject.commits_count).to eq 2
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index fb032a89d50..5da00a8636a 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -31,7 +31,7 @@ describe MergeRequest, models: true do
it { is_expected.to validate_presence_of(:target_branch) }
it { is_expected.to validate_presence_of(:source_branch) }
- context "Validation of merge user with Merge When Build succeeds" do
+ context "Validation of merge user with Merge When Pipeline Succeeds" do
it "allows user to be nil when the feature is disabled" do
expect(subject).to be_valid
end
@@ -142,13 +142,16 @@ describe MergeRequest, models: true do
end
describe '#to_reference' do
+ let(:project) { build(:empty_project, name: 'sample-project') }
+ let(:merge_request) { build(:merge_request, target_project: project, iid: 1) }
+
it 'returns a String reference to the object' do
- expect(subject.to_reference).to eq "!#{subject.iid}"
+ expect(merge_request.to_reference).to eq "!1"
end
it 'supports a cross-project reference' do
- cross = double('project')
- expect(subject.to_reference(cross)).to eq "#{subject.source_project.to_reference}!#{subject.iid}"
+ another_project = build(:project, name: 'another-project', namespace: project.namespace)
+ expect(merge_request.to_reference(another_project)).to eq "sample-project!1"
end
end
@@ -202,7 +205,7 @@ describe MergeRequest, models: true do
end
end
- describe "#mr_and_commit_notes" do
+ describe "#related_notes" do
let!(:merge_request) { create(:merge_request) }
before do
@@ -214,7 +217,7 @@ describe MergeRequest, models: true do
it "includes notes for commits" do
expect(merge_request.commits).not_to be_empty
- expect(merge_request.mr_and_commit_notes.count).to eq(2)
+ expect(merge_request.related_notes.count).to eq(2)
end
it "includes notes for commits from target project as well" do
@@ -222,7 +225,7 @@ describe MergeRequest, models: true do
project: merge_request.target_project)
expect(merge_request.commits).not_to be_empty
- expect(merge_request.mr_and_commit_notes.count).to eq(3)
+ expect(merge_request.related_notes.count).to eq(3)
end
end
@@ -249,7 +252,7 @@ describe MergeRequest, models: true do
end
end
- describe 'detection of issues to be closed' do
+ describe '#closes_issues' do
let(:issue0) { create :issue, project: subject.project }
let(:issue1) { create :issue, project: subject.project }
@@ -277,14 +280,19 @@ describe MergeRequest, models: true do
expect(subject.closes_issues).to be_empty
end
+ end
+
+ describe '#issues_mentioned_but_not_closing' do
+ it 'detects issues mentioned in description but not closed' do
+ mentioned_issue = create(:issue, project: subject.project)
+
+ subject.project.team << [subject.author, :developer]
+ subject.description = "Is related to #{mentioned_issue.to_reference}"
- it 'detects issues mentioned in the description' do
- issue2 = create(:issue, project: subject.project)
- subject.description = "Closes #{issue2.to_reference}"
allow(subject.project).to receive(:default_branch).
and_return(subject.target_branch)
- expect(subject.closes_issues).to include(issue2)
+ expect(subject.issues_mentioned_but_not_closing).to match_array([mentioned_issue])
end
end
@@ -407,11 +415,17 @@ describe MergeRequest, models: true do
.to match("Remove all technical debt\n\n")
end
- it 'includes its description in the body' do
- request = build(:merge_request, description: 'By removing all code')
+ it 'includes its closed issues in the body' do
+ issue = create(:issue, project: subject.project)
- expect(request.merge_commit_message)
- .to match("By removing all code\n\n")
+ subject.project.team << [subject.author, :developer]
+ subject.description = "This issue Closes #{issue.to_reference}"
+
+ allow(subject.project).to receive(:default_branch).
+ and_return(subject.target_branch)
+
+ expect(subject.merge_commit_message)
+ .to match("Closes #{issue.to_reference}")
end
it 'includes its reference in the body' do
@@ -426,6 +440,20 @@ describe MergeRequest, models: true do
expect(request.merge_commit_message).not_to match("Title\n\n\n\n")
end
+
+ it 'includes its description in the body' do
+ request = build(:merge_request, description: 'By removing all code')
+
+ expect(request.merge_commit_message(include_description: true))
+ .to match("By removing all code\n\n")
+ end
+
+ it 'does not includes its description in the body' do
+ request = build(:merge_request, description: 'By removing all code')
+
+ expect(request.merge_commit_message)
+ .not_to match("By removing all code\n\n")
+ end
end
describe "#reset_merge_when_build_succeeds" do
@@ -557,20 +585,17 @@ describe MergeRequest, models: true do
end
describe '#commits_sha' do
- let(:commit0) { double('commit0', sha: 'sha1') }
- let(:commit1) { double('commit1', sha: 'sha2') }
- let(:commit2) { double('commit2', sha: 'sha3') }
-
before do
- allow(subject.merge_request_diff).to receive(:commits).and_return([commit0, commit1, commit2])
+ allow(subject.merge_request_diff).to receive(:commits_sha).
+ and_return(['sha1'])
end
- it 'returns sha of commits' do
- expect(subject.commits_sha).to contain_exactly('sha1', 'sha2', 'sha3')
+ it 'delegates to merge request diff' do
+ expect(subject.commits_sha).to eq ['sha1']
end
end
- describe '#pipeline' do
+ describe '#head_pipeline' do
describe 'when the source project exists' do
it 'returns the latest pipeline' do
pipeline = double(:ci_pipeline, ref: 'master')
@@ -581,7 +606,7 @@ describe MergeRequest, models: true do
with('master', '123abc').
and_return(pipeline)
- expect(subject.pipeline).to eq(pipeline)
+ expect(subject.head_pipeline).to eq(pipeline)
end
end
@@ -589,7 +614,7 @@ describe MergeRequest, models: true do
it 'returns nil' do
allow(subject).to receive(:source_project).and_return(nil)
- expect(subject.pipeline).to be_nil
+ expect(subject.head_pipeline).to be_nil
end
end
end
@@ -856,16 +881,34 @@ describe MergeRequest, models: true do
context 'when it is only allowed to merge when build is green' do
context 'and a failed pipeline is associated' do
before do
- pipeline.statuses << create(:commit_status, status: 'failed', project: project)
- allow(subject).to receive(:pipeline) { pipeline }
+ pipeline.update(status: 'failed')
+ allow(subject).to receive(:head_pipeline) { pipeline }
end
it { expect(subject.mergeable_ci_state?).to be_falsey }
end
+ context 'and a successful pipeline is associated' do
+ before do
+ pipeline.update(status: 'success')
+ allow(subject).to receive(:head_pipeline) { pipeline }
+ end
+
+ it { expect(subject.mergeable_ci_state?).to be_truthy }
+ end
+
+ context 'and a skipped pipeline is associated' do
+ before do
+ pipeline.update(status: 'skipped')
+ allow(subject).to receive(:head_pipeline) { pipeline }
+ end
+
+ it { expect(subject.mergeable_ci_state?).to be_truthy }
+ end
+
context 'when no pipeline is associated' do
before do
- allow(subject).to receive(:pipeline) { nil }
+ allow(subject).to receive(:head_pipeline) { nil }
end
it { expect(subject.mergeable_ci_state?).to be_truthy }
@@ -878,7 +921,7 @@ describe MergeRequest, models: true do
context 'and a failed pipeline is associated' do
before do
pipeline.statuses << create(:commit_status, status: 'failed', project: project)
- allow(subject).to receive(:pipeline) { pipeline }
+ allow(subject).to receive(:head_pipeline) { pipeline }
end
it { expect(subject.mergeable_ci_state?).to be_truthy }
@@ -886,7 +929,7 @@ describe MergeRequest, models: true do
context 'when no pipeline is associated' do
before do
- allow(subject).to receive(:pipeline) { nil }
+ allow(subject).to receive(:head_pipeline) { nil }
end
it { expect(subject.mergeable_ci_state?).to be_truthy }
@@ -919,6 +962,16 @@ describe MergeRequest, models: true do
expect(merge_request.mergeable_discussions_state?).to be_falsey
end
end
+
+ context 'with no discussions' do
+ before do
+ merge_request.notes.destroy_all
+ end
+
+ it 'returns true' do
+ expect(merge_request.mergeable_discussions_state?).to be_truthy
+ end
+ end
end
context 'when project.only_allow_merge_if_all_discussions_are_resolved == false' do
@@ -1099,6 +1152,46 @@ describe MergeRequest, models: true do
allow(subject).to receive(:diff_discussions).and_return([first_discussion, second_discussion, third_discussion])
end
+ describe '#resolvable_discussions' do
+ before do
+ allow(first_discussion).to receive(:to_be_resolved?).and_return(true)
+ allow(second_discussion).to receive(:to_be_resolved?).and_return(false)
+ allow(third_discussion).to receive(:to_be_resolved?).and_return(false)
+ end
+
+ it 'includes only discussions that need to be resolved' do
+ expect(subject.resolvable_discussions).to eq([first_discussion])
+ end
+ end
+
+ describe '#discussions_can_be_resolved_by? user' do
+ let(:user) { build(:user) }
+
+ context 'all discussions can be resolved by the user' do
+ before do
+ allow(first_discussion).to receive(:can_resolve?).with(user).and_return(true)
+ allow(second_discussion).to receive(:can_resolve?).with(user).and_return(true)
+ allow(third_discussion).to receive(:can_resolve?).with(user).and_return(true)
+ end
+
+ it 'allows a user to resolve the discussions' do
+ expect(subject.discussions_can_be_resolved_by?(user)).to be(true)
+ end
+ end
+
+ context 'one discussion cannot be resolved by the user' do
+ before do
+ allow(first_discussion).to receive(:can_resolve?).with(user).and_return(true)
+ allow(second_discussion).to receive(:can_resolve?).with(user).and_return(true)
+ allow(third_discussion).to receive(:can_resolve?).with(user).and_return(false)
+ end
+
+ it 'allows a user to resolve the discussions' do
+ expect(subject.discussions_can_be_resolved_by?(user)).to be(false)
+ end
+ end
+ end
+
describe "#discussions_resolvable?" do
context "when all discussions are unresolvable" do
before do
@@ -1180,6 +1273,50 @@ describe MergeRequest, models: true do
end
end
end
+
+ describe "#discussions_to_be_resolved?" do
+ context "when discussions are not resolvable" do
+ before do
+ allow(subject).to receive(:discussions_resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.discussions_to_be_resolved?).to be false
+ end
+ end
+
+ context "when discussions are resolvable" do
+ before do
+ allow(subject).to receive(:discussions_resolvable?).and_return(true)
+
+ allow(first_discussion).to receive(:resolvable?).and_return(true)
+ allow(second_discussion).to receive(:resolvable?).and_return(false)
+ allow(third_discussion).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable discussions are resolved" do
+ before do
+ allow(first_discussion).to receive(:resolved?).and_return(true)
+ allow(third_discussion).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns false" do
+ expect(subject.discussions_to_be_resolved?).to be false
+ end
+ end
+
+ context "when some resolvable discussions are not resolved" do
+ before do
+ allow(first_discussion).to receive(:resolved?).and_return(true)
+ allow(third_discussion).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns true" do
+ expect(subject.discussions_to_be_resolved?).to be true
+ end
+ end
+ end
+ end
end
describe '#conflicts_can_be_resolved_in_ui?' do
@@ -1368,4 +1505,26 @@ describe MergeRequest, models: true do
end
end
end
+
+ describe '#has_commits?' do
+ before do
+ allow(subject.merge_request_diff).to receive(:commits_count).
+ and_return(2)
+ end
+
+ it 'returns true when merge request diff has commits' do
+ expect(subject.has_commits?).to be_truthy
+ end
+ end
+
+ describe '#has_no_commits?' do
+ before do
+ allow(subject.merge_request_diff).to receive(:commits_count).
+ and_return(0)
+ end
+
+ it 'returns true when merge request diff has 0 commits' do
+ expect(subject.has_no_commits?).to be_truthy
+ end
+ end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 33fe22dd98c..064f29d2d66 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -1,11 +1,6 @@
require 'spec_helper'
describe Milestone, models: true do
- describe "Associations" do
- it { is_expected.to belong_to(:project) }
- it { is_expected.to have_many(:issues) }
- end
-
describe "Validation" do
before do
allow(subject).to receive(:set_iid).and_return(false)
@@ -13,10 +8,25 @@ describe Milestone, models: true do
it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_presence_of(:project) }
+
+ describe 'start_date' do
+ it 'adds an error when start_date is greated then due_date' do
+ milestone = build(:milestone, start_date: Date.tomorrow, due_date: Date.yesterday)
+
+ expect(milestone).not_to be_valid
+ expect(milestone.errors[:start_date]).to include("Can't be greater than due date")
+ end
+ end
+ end
+
+ describe "Associations" do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_many(:issues) }
end
- let(:milestone) { create(:milestone) }
- let(:issue) { create(:issue) }
+ let(:project) { create(:project, :public) }
+ let(:milestone) { create(:milestone, project: project) }
+ let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
describe "#title" do
@@ -58,18 +68,6 @@ describe Milestone, models: true do
end
end
- describe "#expires_at" do
- it "is nil when due_date is unset" do
- milestone.update_attributes(due_date: nil)
- expect(milestone.expires_at).to be_nil
- end
-
- it "is not nil when due_date is set" do
- milestone.update_attributes(due_date: Date.tomorrow)
- expect(milestone.expires_at).to be_present
- end
- end
-
describe '#expired?' do
context "expired" do
before do
@@ -88,6 +86,18 @@ describe Milestone, models: true do
end
end
+ describe '#upcoming?' do
+ it 'returns true' do
+ milestone = build(:milestone, start_date: Time.now + 1.month)
+ expect(milestone.upcoming?).to be_truthy
+ end
+
+ it 'returns false' do
+ milestone = build(:milestone, start_date: Date.today.prev_year)
+ expect(milestone.upcoming?).to be_falsey
+ end
+ end
+
describe '#percent_complete' do
before do
allow(milestone).to receive_messages(
@@ -101,8 +111,8 @@ describe Milestone, models: true do
describe :items_count do
before do
- milestone.issues << create(:issue)
- milestone.issues << create(:closed_issue)
+ milestone.issues << create(:issue, project: project)
+ milestone.issues << create(:closed_issue, project: project)
milestone.merge_requests << create(:merge_request)
end
@@ -117,7 +127,7 @@ describe Milestone, models: true do
describe '#total_items_count' do
before do
- create :closed_issue, milestone: milestone
+ create :closed_issue, milestone: milestone, project: project
create :merge_request, milestone: milestone
end
@@ -237,4 +247,18 @@ describe Milestone, models: true do
end
end
end
+
+ describe '#to_reference' do
+ let(:project) { build(:empty_project, name: 'sample-project') }
+ let(:milestone) { build(:milestone, iid: 1, project: project) }
+
+ it 'returns a String reference to the object' do
+ expect(milestone.to_reference).to eq "%1"
+ end
+
+ it 'supports a cross-project reference' do
+ another_project = build(:project, name: 'another-project', namespace: project.namespace)
+ expect(milestone.to_reference(another_project)).to eq "sample-project%1"
+ end
+ end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 431b3e4435f..9fd06bb6b23 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -4,14 +4,17 @@ describe Namespace, models: true do
let!(:namespace) { create(:namespace) }
it { is_expected.to have_many :projects }
- it { is_expected.to validate_presence_of :name }
- it { is_expected.to validate_uniqueness_of(:name) }
- it { is_expected.to validate_presence_of :path }
- it { is_expected.to validate_uniqueness_of(:path) }
- it { is_expected.to validate_presence_of :owner }
- describe "Mass assignment" do
- 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) }
+
+ 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) }
describe "Respond to" do
it { is_expected.to respond_to(:human_name) }
@@ -117,4 +120,34 @@ describe Namespace, models: true do
expect(Namespace.clean_path("--%+--valid_*&%name=.git.%.atom.atom.@email.com")).to eq("valid_name")
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 '#parents' do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+ let(:deep_nested_group) { create(:group, parent: nested_group) }
+ let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
+
+ it 'returns the correct parents' do
+ expect(very_deep_nested_group.parents).to eq([group, nested_group, deep_nested_group])
+ expect(deep_nested_group.parents).to eq([group, nested_group])
+ expect(nested_group.parents).to eq([group])
+ expect(group.parents).to eq([])
+ end
+ end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index e6b6e7c0634..310fecd8a5c 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -162,44 +162,6 @@ describe Note, models: true do
end
end
- describe '.search' do
- let(:note) { create(:note_on_issue, note: 'WoW') }
-
- it 'returns notes with matching content' do
- expect(described_class.search(note.note)).to eq([note])
- end
-
- it 'returns notes with matching content regardless of the casing' do
- expect(described_class.search('WOW')).to eq([note])
- end
-
- context "confidential issues" do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
- let(:confidential_note) { create(:note, note: "Random", noteable: confidential_issue, project: confidential_issue.project) }
-
- it "returns notes with matching content if user can see the issue" do
- expect(described_class.search(confidential_note.note, as_user: user)).to eq([confidential_note])
- end
-
- it "does not return notes with matching content if user can not see the issue" do
- user = create(:user)
- expect(described_class.search(confidential_note.note, as_user: user)).to be_empty
- end
-
- it "does not return notes with matching content for project members with guest role" do
- user = create(:user)
- project.team << [user, :guest]
- expect(described_class.search(confidential_note.note, as_user: user)).to be_empty
- end
-
- it "does not return notes with matching content for unauthenticated users" do
- expect(described_class.search(confidential_note.note)).to be_empty
- end
- end
- end
-
describe "editable?" do
it "returns true" do
note = build(:note)
@@ -223,7 +185,7 @@ describe Note, models: true do
let(:note) do
create :note,
noteable: ext_issue, project: ext_proj,
- note: "Mentioned in issue #{private_issue.to_reference(ext_proj)}",
+ note: "mentioned in issue #{private_issue.to_reference(ext_proj)}",
system: true
end
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
index c5ff1941378..47397a822c1 100644
--- a/spec/models/project_group_link_spec.rb
+++ b/spec/models/project_group_link_spec.rb
@@ -14,4 +14,20 @@ describe ProjectGroupLink do
it { should validate_presence_of(:group) }
it { should validate_presence_of(:group_access) }
end
+
+ describe "destroying a record", truncate: true do
+ it "refreshes group users' authorized projects" do
+ project = create(:project, :private)
+ group = create(:group)
+ reporter = create(:user)
+ group_users = group.users
+
+ group.add_reporter(reporter)
+ project.project_group_links.create(group: group)
+ group_users.each { |user| expect(user.authorized_projects).to include(project) }
+
+ project.project_group_links.destroy_all
+ group_users.each { |user| expect(user.authorized_projects).not_to include(project) }
+ end
+ end
end
diff --git a/spec/models/project_label_spec.rb b/spec/models/project_label_spec.rb
index 18c9d449ee5..4d538cac007 100644
--- a/spec/models/project_label_spec.rb
+++ b/spec/models/project_label_spec.rb
@@ -105,14 +105,14 @@ describe ProjectLabel, models: true do
context 'using name' do
it 'returns cross reference with label name' do
expect(label.to_reference(project, format: :name))
- .to eq %Q(#{label.project.to_reference}~"#{label.name}")
+ .to eq %Q(#{label.project.path_with_namespace}~"#{label.name}")
end
end
context 'using id' do
it 'returns cross reference with label id' do
expect(label.to_reference(project, format: :id))
- .to eq %Q(#{label.project.to_reference}~#{label.id})
+ .to eq %Q(#{label.project.path_with_namespace}~#{label.id})
end
end
end
diff --git a/spec/models/project_services/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb
new file mode 100644
index 00000000000..b71d153f814
--- /dev/null
+++ b/spec/models/project_services/chat_message/build_message_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe ChatMessage::BuildMessage do
+ subject { described_class.new(args) }
+
+ let(:args) do
+ {
+ sha: '97de212e80737a608d939f648d959671fb0a0142',
+ ref: 'develop',
+ tag: false,
+
+ project_name: 'project_name',
+ project_url: 'example.gitlab.com',
+
+ commit: {
+ status: status,
+ author_name: 'hacker',
+ duration: duration,
+ },
+ }
+ end
+
+ let(:message) { build_message }
+
+ context 'build succeeded' do
+ let(:status) { 'success' }
+ let(:color) { 'good' }
+ let(:duration) { 10 }
+ let(:message) { build_message('passed') }
+
+ it 'returns a message with information about succeeded build' do
+ expect(subject.pretext).to be_empty
+ expect(subject.fallback).to eq(message)
+ expect(subject.attachments).to eq([text: message, color: color])
+ end
+ end
+
+ context 'build failed' do
+ let(:status) { 'failed' }
+ let(:color) { 'danger' }
+ let(:duration) { 10 }
+
+ it 'returns a message with information about failed build' do
+ expect(subject.pretext).to be_empty
+ expect(subject.fallback).to eq(message)
+ expect(subject.attachments).to eq([text: message, color: color])
+ end
+ end
+
+ def build_message(status_text = status)
+ "<example.gitlab.com|project_name>:" \
+ " Commit <example.gitlab.com/commit/" \
+ "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \
+ " of <example.gitlab.com/commits/develop|develop> branch" \
+ " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
+ end
+end
diff --git a/spec/models/project_services/chat_message/issue_message_spec.rb b/spec/models/project_services/chat_message/issue_message_spec.rb
new file mode 100644
index 00000000000..ebe0ead4408
--- /dev/null
+++ b/spec/models/project_services/chat_message/issue_message_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe ChatMessage::IssueMessage, models: true do
+ subject { described_class.new(args) }
+
+ let(:args) do
+ {
+ user: {
+ name: 'Test User',
+ username: 'test.user'
+ },
+ project_name: 'project_name',
+ project_url: 'somewhere.com',
+
+ object_attributes: {
+ title: 'Issue title',
+ id: 10,
+ iid: 100,
+ assignee_id: 1,
+ url: 'url',
+ action: 'open',
+ state: 'opened',
+ description: 'issue description'
+ }
+ }
+ end
+
+ let(:color) { '#C95823' }
+
+ context '#initialize' do
+ before do
+ args[:object_attributes][:description] = nil
+ end
+
+ it 'returns a non-null description' do
+ expect(subject.description).to eq('')
+ end
+ end
+
+ context 'open' do
+ it 'returns a message regarding opening of issues' do
+ expect(subject.pretext).to eq(
+ '<somewhere.com|[project_name>] Issue opened by test.user')
+ expect(subject.attachments).to eq([
+ {
+ title: "#100 Issue title",
+ title_link: "url",
+ text: "issue description",
+ color: color,
+ }
+ ])
+ end
+ end
+
+ context 'close' do
+ before do
+ args[:object_attributes][:action] = 'close'
+ args[:object_attributes][:state] = 'closed'
+ end
+
+ it 'returns a message regarding closing of issues' do
+ expect(subject.pretext). to eq(
+ '<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by test.user')
+ expect(subject.attachments).to be_empty
+ end
+ end
+end
diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb
new file mode 100644
index 00000000000..07c414c6ca4
--- /dev/null
+++ b/spec/models/project_services/chat_message/merge_message_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe ChatMessage::MergeMessage, models: true do
+ subject { described_class.new(args) }
+
+ let(:args) do
+ {
+ user: {
+ name: 'Test User',
+ username: 'test.user'
+ },
+ project_name: 'project_name',
+ project_url: 'somewhere.com',
+
+ object_attributes: {
+ title: "Issue title\nSecond line",
+ id: 10,
+ iid: 100,
+ assignee_id: 1,
+ url: 'url',
+ state: 'opened',
+ description: 'issue description',
+ source_branch: 'source_branch',
+ target_branch: 'target_branch',
+ }
+ }
+ end
+
+ let(:color) { '#345' }
+
+ context 'open' do
+ it 'returns a message regarding opening of merge requests' do
+ expect(subject.pretext).to eq(
+ 'test.user opened <somewhere.com/merge_requests/100|merge request !100> '\
+ 'in <somewhere.com|project_name>: *Issue title*')
+ expect(subject.attachments).to be_empty
+ end
+ end
+
+ context 'close' do
+ before do
+ args[:object_attributes][:state] = 'closed'
+ end
+ it 'returns a message regarding closing of merge requests' do
+ expect(subject.pretext).to eq(
+ 'test.user closed <somewhere.com/merge_requests/100|merge request !100> '\
+ 'in <somewhere.com|project_name>: *Issue title*')
+ expect(subject.attachments).to be_empty
+ end
+ end
+end
diff --git a/spec/models/project_services/chat_message/note_message_spec.rb b/spec/models/project_services/chat_message/note_message_spec.rb
new file mode 100644
index 00000000000..31936da40a2
--- /dev/null
+++ b/spec/models/project_services/chat_message/note_message_spec.rb
@@ -0,0 +1,130 @@
+require 'spec_helper'
+
+describe ChatMessage::NoteMessage, models: true do
+ let(:color) { '#345' }
+
+ before do
+ @args = {
+ user: {
+ name: 'Test User',
+ username: 'test.user',
+ avatar_url: 'http://fakeavatar'
+ },
+ project_name: 'project_name',
+ project_url: 'somewhere.com',
+ repository: {
+ name: 'project_name',
+ url: 'somewhere.com',
+ },
+ object_attributes: {
+ id: 10,
+ note: 'comment on a commit',
+ url: 'url',
+ noteable_type: 'Commit'
+ }
+ }
+ end
+
+ context 'commit notes' do
+ before do
+ @args[:object_attributes][:note] = 'comment on a commit'
+ @args[:object_attributes][:noteable_type] = 'Commit'
+ @args[:commit] = {
+ id: '5f163b2b95e6f53cbd428f5f0b103702a52b9a23',
+ message: "Added a commit message\ndetails\n123\n"
+ }
+ end
+
+ it 'returns a message regarding notes on commits' do
+ message = described_class.new(@args)
+ expect(message.pretext).to eq("test.user <url|commented on " \
+ "commit 5f163b2b> in <somewhere.com|project_name>: " \
+ "*Added a commit message*")
+ expected_attachments = [
+ {
+ text: "comment on a commit",
+ color: color,
+ }
+ ]
+ expect(message.attachments).to eq(expected_attachments)
+ end
+ end
+
+ context 'merge request notes' do
+ before do
+ @args[:object_attributes][:note] = 'comment on a merge request'
+ @args[:object_attributes][:noteable_type] = 'MergeRequest'
+ @args[:merge_request] = {
+ id: 1,
+ iid: 30,
+ title: "merge request title\ndetails\n"
+ }
+ end
+
+ it 'returns a message regarding notes on a merge request' do
+ message = described_class.new(@args)
+ expect(message.pretext).to eq("test.user <url|commented on " \
+ "merge request !30> in <somewhere.com|project_name>: " \
+ "*merge request title*")
+ expected_attachments = [
+ {
+ text: "comment on a merge request",
+ color: color,
+ }
+ ]
+ expect(message.attachments).to eq(expected_attachments)
+ end
+ end
+
+ context 'issue notes' do
+ before do
+ @args[:object_attributes][:note] = 'comment on an issue'
+ @args[:object_attributes][:noteable_type] = 'Issue'
+ @args[:issue] = {
+ id: 1,
+ iid: 20,
+ title: "issue title\ndetails\n"
+ }
+ end
+
+ it 'returns a message regarding notes on an issue' do
+ message = described_class.new(@args)
+ expect(message.pretext).to eq(
+ "test.user <url|commented on " \
+ "issue #20> in <somewhere.com|project_name>: " \
+ "*issue title*")
+ expected_attachments = [
+ {
+ text: "comment on an issue",
+ color: color,
+ }
+ ]
+ expect(message.attachments).to eq(expected_attachments)
+ end
+ end
+
+ context 'project snippet notes' do
+ before do
+ @args[:object_attributes][:note] = 'comment on a snippet'
+ @args[:object_attributes][:noteable_type] = 'Snippet'
+ @args[:snippet] = {
+ id: 5,
+ title: "snippet title\ndetails\n"
+ }
+ end
+
+ it 'returns a message regarding notes on a project snippet' do
+ message = described_class.new(@args)
+ expect(message.pretext).to eq("test.user <url|commented on " \
+ "snippet #5> in <somewhere.com|project_name>: " \
+ "*snippet title*")
+ expected_attachments = [
+ {
+ text: "comment on a snippet",
+ color: color,
+ }
+ ]
+ expect(message.attachments).to eq(expected_attachments)
+ end
+ end
+end
diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb
new file mode 100644
index 00000000000..eca71db07b6
--- /dev/null
+++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe ChatMessage::PipelineMessage do
+ subject { described_class.new(args) }
+ let(:user) { { name: 'hacker' } }
+
+ let(:args) do
+ {
+ object_attributes: {
+ id: 123,
+ sha: '97de212e80737a608d939f648d959671fb0a0142',
+ tag: false,
+ ref: 'develop',
+ status: status,
+ duration: duration
+ },
+ project: { path_with_namespace: 'project_name',
+ web_url: 'example.gitlab.com' },
+ user: user
+ }
+ end
+
+ let(:message) { build_message }
+
+ context 'pipeline succeeded' do
+ let(:status) { 'success' }
+ let(:color) { 'good' }
+ let(:duration) { 10 }
+ let(:message) { build_message('passed') }
+
+ it 'returns a message with information about succeeded build' do
+ verify_message
+ end
+ end
+
+ context 'pipeline failed' do
+ let(:status) { 'failed' }
+ let(:color) { 'danger' }
+ let(:duration) { 10 }
+
+ it 'returns a message with information about failed build' do
+ verify_message
+ end
+
+ context 'when triggered by API therefore lacking user' do
+ let(:user) { nil }
+ let(:message) { build_message(status, 'API') }
+
+ it 'returns a message stating it is by API' do
+ verify_message
+ end
+ end
+ end
+
+ def verify_message
+ expect(subject.pretext).to be_empty
+ expect(subject.fallback).to eq(message)
+ expect(subject.attachments).to eq([text: message, color: color])
+ end
+
+ def build_message(status_text = status, name = user[:name])
+ "<example.gitlab.com|project_name>:" \
+ " Pipeline <example.gitlab.com/pipelines/123|#123>" \
+ " of <example.gitlab.com/commits/develop|develop> branch" \
+ " by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}"
+ end
+end
diff --git a/spec/models/project_services/chat_message/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb
new file mode 100644
index 00000000000..b781c4505db
--- /dev/null
+++ b/spec/models/project_services/chat_message/push_message_spec.rb
@@ -0,0 +1,88 @@
+require 'spec_helper'
+
+describe ChatMessage::PushMessage, models: true do
+ subject { described_class.new(args) }
+
+ let(:args) do
+ {
+ after: 'after',
+ before: 'before',
+ project_name: 'project_name',
+ ref: 'refs/heads/master',
+ user_name: 'test.user',
+ project_url: 'url'
+ }
+ end
+
+ let(:color) { '#345' }
+
+ context 'push' do
+ before do
+ args[:commits] = [
+ { message: 'message1', url: 'url1', id: 'abcdefghijkl', author: { name: 'author1' } },
+ { message: 'message2', url: 'url2', id: '123456789012', author: { name: 'author2' } },
+ ]
+ end
+
+ it 'returns a message regarding pushes' do
+ expect(subject.pretext).to eq(
+ 'test.user pushed to branch <url/commits/master|master> of '\
+ '<url|project_name> (<url/compare/before...after|Compare changes>)'
+ )
+ expect(subject.attachments).to eq([
+ {
+ text: "<url1|abcdefgh>: message1 - author1\n"\
+ "<url2|12345678>: message2 - author2",
+ color: color,
+ }
+ ])
+ end
+ end
+
+ context 'tag push' do
+ let(:args) do
+ {
+ after: 'after',
+ before: Gitlab::Git::BLANK_SHA,
+ project_name: 'project_name',
+ ref: 'refs/tags/new_tag',
+ user_name: 'test.user',
+ project_url: 'url'
+ }
+ end
+
+ it 'returns a message regarding pushes' do
+ expect(subject.pretext).to eq('test.user pushed new tag ' \
+ '<url/commits/new_tag|new_tag> to ' \
+ '<url|project_name>')
+ expect(subject.attachments).to be_empty
+ end
+ end
+
+ context 'new branch' do
+ before do
+ args[:before] = Gitlab::Git::BLANK_SHA
+ end
+
+ it 'returns a message regarding a new branch' do
+ expect(subject.pretext).to eq(
+ 'test.user pushed new branch <url/commits/master|master> to '\
+ '<url|project_name>'
+ )
+ expect(subject.attachments).to be_empty
+ end
+ end
+
+ context 'removed branch' do
+ before do
+ args[:after] = Gitlab::Git::BLANK_SHA
+ end
+
+ it 'returns a message regarding a removed branch' do
+ expect(subject.pretext).to eq(
+ 'test.user removed branch master from <url|project_name>'
+ )
+ expect(subject.attachments).to be_empty
+ end
+ end
+end
diff --git a/spec/models/project_services/chat_message/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb
new file mode 100644
index 00000000000..94c04dc0865
--- /dev/null
+++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe ChatMessage::WikiPageMessage, models: true do
+ subject { described_class.new(args) }
+
+ let(:args) do
+ {
+ user: {
+ name: 'Test User',
+ username: 'test.user'
+ },
+ project_name: 'project_name',
+ project_url: 'somewhere.com',
+ object_attributes: {
+ title: 'Wiki page title',
+ url: 'url',
+ content: 'Wiki page description'
+ }
+ }
+ end
+
+ describe '#pretext' do
+ context 'when :action == "create"' do
+ before { args[:object_attributes][:action] = 'create' }
+
+ it 'returns a message that a new wiki page was created' do
+ expect(subject.pretext).to eq(
+ 'test.user created <url|wiki page> in <somewhere.com|project_name>: '\
+ '*Wiki page title*')
+ end
+ end
+
+ context 'when :action == "update"' do
+ before { args[:object_attributes][:action] = 'update' }
+
+ it 'returns a message that a wiki page was updated' do
+ expect(subject.pretext).to eq(
+ 'test.user edited <url|wiki page> in <somewhere.com|project_name>: '\
+ '*Wiki page title*')
+ end
+ end
+ end
+
+ describe '#attachments' do
+ let(:color) { '#345' }
+
+ context 'when :action == "create"' do
+ before { args[:object_attributes][:action] = 'create' }
+
+ it 'returns the attachment for a new wiki page' do
+ expect(subject.attachments).to eq([
+ {
+ text: "Wiki page description",
+ color: color,
+ }
+ ])
+ end
+ end
+
+ context 'when :action == "update"' do
+ before { args[:object_attributes][:action] = 'update' }
+
+ it 'returns the attachment for an updated wiki page' do
+ expect(subject.attachments).to eq([
+ {
+ text: "Wiki page description",
+ color: color,
+ }
+ ])
+ end
+ end
+ end
+end
diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb
new file mode 100644
index 00000000000..c98e7ee14fd
--- /dev/null
+++ b/spec/models/project_services/chat_notification_service_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe ChatNotificationService, models: true do
+ describe "Associations" do
+ before do
+ allow(subject).to receive(:activated?).and_return(true)
+ end
+
+ it { is_expected.to validate_presence_of :webhook }
+ end
+end
diff --git a/spec/models/project_services/chat_service_spec.rb b/spec/models/project_services/chat_service_spec.rb
new file mode 100644
index 00000000000..c6a45a3e1be
--- /dev/null
+++ b/spec/models/project_services/chat_service_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe ChatService, models: true do
+ describe "Associations" do
+ it { is_expected.to have_many :chat_names }
+ end
+
+ describe '#valid_token?' do
+ subject { described_class.new }
+
+ it 'is false as it has no token' do
+ expect(subject.valid_token?('wer')).to be_falsey
+ end
+ end
+end
diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
index 652804fb444..9b80f0e7296 100644
--- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
@@ -35,9 +35,9 @@ describe GitlabIssueTrackerService, models: true do
end
it 'gives the correct path' do
- expect(@service.project_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues")
- expect(@service.new_issue_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/new")
- expect(@service.issue_url(432)).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/432")
+ expect(@service.project_url).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.path_with_namespace}/issues")
+ expect(@service.new_issue_url).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.path_with_namespace}/issues/new")
+ expect(@service.issue_url(432)).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.path_with_namespace}/issues/432")
end
end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 05ee4a08391..862e3a72a73 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -68,7 +68,8 @@ describe JiraService, models: true do
end
end
- describe "Execute" do
+ describe '#close_issue' do
+ let(:custom_base_url) { 'http://custom_url' }
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:merge_request) { create(:merge_request) }
@@ -82,35 +83,80 @@ describe JiraService, models: true do
url: 'http://jira.example.com',
username: 'gitlab_jira_username',
password: 'gitlab_jira_password',
- project_key: 'GitLabProject'
+ project_key: 'GitLabProject',
+ jira_issue_transition_id: "custom-id"
)
+ # These stubs are needed to test JiraService#close_issue.
+ # We close the issue then do another request to API to check if it got closed.
+ # Here is stubbed the API return with a closed and an opened issues.
+ open_issue = JIRA::Resource::Issue.new(@jira_service.client, attrs: { "id" => "JIRA-123" })
+ closed_issue = open_issue.dup
+ allow(open_issue).to receive(:resolution).and_return(false)
+ allow(closed_issue).to receive(:resolution).and_return(true)
+ allow(JIRA::Resource::Issue).to receive(:find).and_return(open_issue, closed_issue)
+
+ allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return("JIRA-123")
+
@jira_service.save
project_issues_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123'
- @project_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/project/GitLabProject'
@transitions_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions'
@comment_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment'
+ @remote_link_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/remotelink'
- WebMock.stub_request(:get, @project_url)
WebMock.stub_request(:get, project_issues_url)
WebMock.stub_request(:post, @transitions_url)
WebMock.stub_request(:post, @comment_url)
+ WebMock.stub_request(:post, @remote_link_url)
end
it "calls JIRA API" do
- @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
+ @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @comment_url).with(
body: /Issue solved with/
).once
end
+ # Check https://developer.atlassian.com/jiradev/jira-platform/guides/other/guide-jira-remote-issue-links/fields-in-remote-issue-links
+ # for more information
+ it "creates Remote Link reference in JIRA for comment" do
+ @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
+
+ # Creates comment
+ expect(WebMock).to have_requested(:post, @comment_url)
+
+ # Creates Remote Link in JIRA issue fields
+ expect(WebMock).to have_requested(:post, @remote_link_url).with(
+ body: hash_including(
+ GlobalID: "GitLab",
+ object: {
+ url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/#{merge_request.diff_head_sha}",
+ title: "GitLab: Solved by commit #{merge_request.diff_head_sha}.",
+ icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" },
+ status: { resolved: true, icon: { url16x16: "http://www.openwebgraphics.com/resources/data/1768/16x16_apply.png", title: "Closed" } }
+ }
+ )
+ ).once
+ end
+
+ it "does not send comment or remote links to issues already closed" do
+ allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(true)
+
+ @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
+
+ expect(WebMock).not_to have_requested(:post, @comment_url)
+ expect(WebMock).not_to have_requested(:post, @remote_link_url)
+ end
+
it "references the GitLab commit/merge request" do
- @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
+ stub_config_setting(base_url: custom_base_url)
+
+ @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @comment_url).with(
- body: /#{Gitlab.config.gitlab.url}\/#{project.path_with_namespace}\/commit\/#{merge_request.diff_head_sha}/
+ body: /#{custom_base_url}\/#{project.path_with_namespace}\/commit\/#{merge_request.diff_head_sha}/
).once
end
@@ -122,7 +168,7 @@ describe JiraService, models: true do
{ script_name: '/gitlab' }
end
- @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
+ @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @comment_url).with(
body: /#{Gitlab.config.gitlab.url}\/#{project.path_with_namespace}\/commit\/#{merge_request.diff_head_sha}/
@@ -130,20 +176,33 @@ describe JiraService, models: true do
end
it "calls the api with jira_issue_transition_id" do
- @jira_service.jira_issue_transition_id = 'this-is-a-custom-id'
- @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
+ @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @transitions_url).with(
- body: /this-is-a-custom-id/
+ body: /custom-id/
).once
end
+ end
- context "when testing" do
- it "tries to get jira project" do
- @jira_service.execute(nil)
+ describe '#test_settings' do
+ let(:jira_service) do
+ described_class.new(
+ url: 'http://jira.example.com',
+ username: 'gitlab_jira_username',
+ password: 'gitlab_jira_password',
+ project_key: 'GitLabProject'
+ )
+ end
+ let(:project_url) { 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/project/GitLabProject' }
- expect(WebMock).to have_requested(:get, @project_url)
- end
+ before do
+ WebMock.stub_request(:get, project_url)
+ end
+
+ it 'tries to get JIRA project' do
+ jira_service.test_settings
+
+ expect(WebMock).to have_requested(:get, project_url)
end
end
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
new file mode 100644
index 00000000000..3603602e41d
--- /dev/null
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -0,0 +1,159 @@
+require 'spec_helper'
+
+describe KubernetesService, models: true do
+ let(:project) { create(:empty_project) }
+
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ end
+
+ describe 'Validations' do
+ context 'when service is active' do
+ before { subject.active = true }
+ it { is_expected.to validate_presence_of(:namespace) }
+ it { is_expected.to validate_presence_of(:api_url) }
+ it { is_expected.to validate_presence_of(:token) }
+
+ context 'namespace format' do
+ before do
+ subject.project = project
+ subject.api_url = "http://example.com"
+ subject.token = "test"
+ end
+
+ {
+ 'foo' => true,
+ '1foo' => true,
+ 'foo1' => true,
+ 'foo-bar' => true,
+ '-foo' => false,
+ 'foo-' => false,
+ 'a' * 63 => true,
+ 'a' * 64 => false,
+ 'a.b' => false,
+ 'a*b' => false,
+ }.each do |namespace, validity|
+ it "should validate #{namespace} as #{validity ? 'valid' : 'invalid'}" do
+ subject.namespace = namespace
+
+ expect(subject.valid?).to eq(validity)
+ end
+ end
+ end
+ end
+
+ context 'when service is inactive' do
+ before { subject.active = false }
+ it { is_expected.not_to validate_presence_of(:namespace) }
+ it { is_expected.not_to validate_presence_of(:api_url) }
+ it { is_expected.not_to validate_presence_of(:token) }
+ end
+ end
+
+ describe '#initialize_properties' do
+ context 'with a project' do
+ it 'defaults to the project name' do
+ expect(described_class.new(project: project).namespace).to eq(project.name)
+ end
+ end
+
+ context 'without a project' do
+ it 'leaves the namespace unset' do
+ expect(described_class.new.namespace).to be_nil
+ end
+ end
+ end
+
+ describe '#test' do
+ let(:project) { create(:kubernetes_project) }
+ let(:service) { project.kubernetes_service }
+ let(:discovery_url) { service.api_url + '/api/v1' }
+
+ # JSON response body from Kubernetes GET /api/v1 request
+ let(:discovery_response) { { "kind" => "APIResourceList", "groupVersion" => "v1", "resources" => [] }.to_json }
+
+ context 'with path prefix in api_url' do
+ let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' }
+
+ before do
+ service.api_url = 'https://kubernetes.example.com/prefix/'
+ end
+
+ it 'tests with the prefix' do
+ WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response)
+
+ expect(service.test[:success]).to be_truthy
+ expect(WebMock).to have_requested(:get, discovery_url).once
+ end
+ end
+
+ context 'with custom CA certificate' do
+ let(:certificate) { "CA PEM DATA" }
+ before do
+ service.update_attributes!(ca_pem: certificate)
+ end
+
+ it 'is added to the certificate store' do
+ cert = double("certificate")
+
+ expect(OpenSSL::X509::Certificate).to receive(:new).with(certificate).and_return(cert)
+ expect_any_instance_of(OpenSSL::X509::Store).to receive(:add_cert).with(cert)
+ WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response)
+
+ expect(service.test[:success]).to be_truthy
+ expect(WebMock).to have_requested(:get, discovery_url).once
+ end
+ end
+
+ context 'success' do
+ it 'reads the discovery endpoint' do
+ WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response)
+
+ expect(service.test[:success]).to be_truthy
+ expect(WebMock).to have_requested(:get, discovery_url).once
+ end
+ end
+
+ context 'failure' do
+ it 'fails to read the discovery endpoint' do
+ WebMock.stub_request(:get, discovery_url).to_return(status: 404)
+
+ expect(service.test[:success]).to be_falsy
+ expect(WebMock).to have_requested(:get, discovery_url).once
+ end
+ end
+ end
+
+ describe '#predefined_variables' do
+ before do
+ subject.api_url = 'https://kube.domain.com'
+ subject.token = 'token'
+ subject.namespace = 'my-project'
+ subject.ca_pem = 'CA PEM DATA'
+ end
+
+ it 'sets KUBE_URL' do
+ expect(subject.predefined_variables).to include(
+ { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true }
+ )
+ end
+
+ it 'sets KUBE_TOKEN' do
+ expect(subject.predefined_variables).to include(
+ { key: 'KUBE_TOKEN', value: 'token', public: false }
+ )
+ end
+
+ it 'sets KUBE_NAMESPACE' do
+ expect(subject.predefined_variables).to include(
+ { key: 'KUBE_NAMESPACE', value: 'my-project', public: true }
+ )
+ end
+
+ it 'sets KUBE_CA_PEM' do
+ expect(subject.predefined_variables).to include(
+ { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true }
+ )
+ end
+ end
+end
diff --git a/spec/models/project_services/mattermost_notification_service_spec.rb b/spec/models/project_services/mattermost_notification_service_spec.rb
new file mode 100644
index 00000000000..c01e64b4c8e
--- /dev/null
+++ b/spec/models/project_services/mattermost_notification_service_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe MattermostNotificationService, models: true do
+ it_behaves_like "slack or mattermost"
+end
diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
new file mode 100644
index 00000000000..4a1037e950b
--- /dev/null
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -0,0 +1,99 @@
+require 'spec_helper'
+
+describe MattermostSlashCommandsService, models: true do
+ describe "Associations" do
+ it { is_expected.to respond_to :token }
+ end
+
+ describe '#valid_token?' do
+ subject { described_class.new }
+
+ context 'when the token is empty' do
+ it 'is false' do
+ expect(subject.valid_token?('wer')).to be_falsey
+ end
+ end
+
+ context 'when there is a token' do
+ before do
+ subject.token = '123'
+ end
+
+ it 'accepts equal tokens' do
+ expect(subject.valid_token?('123')).to be_truthy
+ end
+ end
+ end
+
+ describe '#trigger' do
+ subject { described_class.new }
+
+ context 'no token is passed' do
+ let(:params) { Hash.new }
+
+ it 'returns nil' do
+ expect(subject.trigger(params)).to be_nil
+ end
+ end
+
+ context 'with a token passed' do
+ let(:project) { create(:empty_project) }
+ let(:params) { { token: 'token' } }
+
+ before do
+ allow(subject).to receive(:token).and_return('token')
+ end
+
+ context 'no user can be found' do
+ context 'when no url can be generated' do
+ it 'responds with the authorize url' do
+ response = subject.trigger(params)
+
+ expect(response[:response_type]).to eq :ephemeral
+ expect(response[:text]).to start_with ":sweat_smile: Couldn't identify you"
+ end
+ end
+
+ context 'when an auth url can be generated' do
+ let(:params) do
+ {
+ team_domain: 'http://domain.tld',
+ team_id: 'T3423423',
+ user_id: 'U234234',
+ user_name: 'mepmep',
+ token: 'token'
+ }
+ end
+
+ let(:service) do
+ project.create_mattermost_slash_commands_service(
+ properties: { token: 'token' }
+ )
+ end
+
+ it 'generates the url' do
+ response = service.trigger(params)
+
+ expect(response[:text]).to start_with(':wave: Hi there!')
+ end
+ end
+ end
+
+ context 'when the user is authenticated' do
+ let!(:chat_name) { create(:chat_name, service: service) }
+ let(:service) do
+ project.create_mattermost_slash_commands_service(
+ properties: { token: 'token' }
+ )
+ end
+ let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } }
+
+ it 'triggers the command' do
+ expect_any_instance_of(Gitlab::ChatCommands::Command).to receive(:execute)
+
+ service.trigger(params)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/project_services/pipeline_email_service_spec.rb b/spec/models/project_services/pipeline_email_service_spec.rb
index 1368a2925e8..7c8824485f5 100644
--- a/spec/models/project_services/pipeline_email_service_spec.rb
+++ b/spec/models/project_services/pipeline_email_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe PipelinesEmailService do
+ include EmailHelpers
+
let(:pipeline) do
create(:ci_pipeline, project: project, sha: project.commit('master').sha)
end
@@ -13,7 +15,7 @@ describe PipelinesEmailService do
end
before do
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
end
describe 'Validations' do
@@ -23,14 +25,6 @@ describe PipelinesEmailService do
end
it { is_expected.to validate_presence_of(:recipients) }
-
- context 'when pusher is added' do
- before do
- subject.add_pusher = true
- end
-
- it { is_expected.not_to validate_presence_of(:recipients) }
- end
end
context 'when service is inactive' do
@@ -66,8 +60,7 @@ describe PipelinesEmailService do
end
it 'sends email' do
- sent_to = ActionMailer::Base.deliveries.flat_map(&:to)
- expect(sent_to).to contain_exactly(recipient)
+ should_only_email(double(notification_email: recipient), kind: :bcc)
end
end
@@ -79,7 +72,7 @@ describe PipelinesEmailService do
end
it 'does not send email' do
- expect(ActionMailer::Base.deliveries).to be_empty
+ should_not_email_anyone
end
end
diff --git a/spec/models/project_services/slack_notification_service_spec.rb b/spec/models/project_services/slack_notification_service_spec.rb
new file mode 100644
index 00000000000..59ddddf7454
--- /dev/null
+++ b/spec/models/project_services/slack_notification_service_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe SlackNotificationService, models: true do
+ it_behaves_like "slack or mattermost"
+end
diff --git a/spec/models/project_services/slack_service/build_message_spec.rb b/spec/models/project_services/slack_service/build_message_spec.rb
deleted file mode 100644
index 452f4e2782c..00000000000
--- a/spec/models/project_services/slack_service/build_message_spec.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-require 'spec_helper'
-
-describe SlackService::BuildMessage do
- subject { SlackService::BuildMessage.new(args) }
-
- let(:args) do
- {
- sha: '97de212e80737a608d939f648d959671fb0a0142',
- ref: 'develop',
- tag: false,
-
- project_name: 'project_name',
- project_url: 'example.gitlab.com',
-
- commit: {
- status: status,
- author_name: 'hacker',
- duration: duration,
- },
- }
- end
-
- let(:message) { build_message }
-
- context 'build succeeded' do
- let(:status) { 'success' }
- let(:color) { 'good' }
- let(:duration) { 10 }
- let(:message) { build_message('passed') }
-
- it 'returns a message with information about succeeded build' do
- expect(subject.pretext).to be_empty
- expect(subject.fallback).to eq(message)
- expect(subject.attachments).to eq([text: message, color: color])
- end
- end
-
- context 'build failed' do
- let(:status) { 'failed' }
- let(:color) { 'danger' }
- let(:duration) { 10 }
-
- it 'returns a message with information about failed build' do
- expect(subject.pretext).to be_empty
- expect(subject.fallback).to eq(message)
- expect(subject.attachments).to eq([text: message, color: color])
- end
- end
-
- def build_message(status_text = status)
- "<example.gitlab.com|project_name>:" \
- " Commit <example.gitlab.com/commit/" \
- "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \
- " of <example.gitlab.com/commits/develop|develop> branch" \
- " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
- end
-end
diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/slack_service/issue_message_spec.rb
deleted file mode 100644
index 98c36ec088d..00000000000
--- a/spec/models/project_services/slack_service/issue_message_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-require 'spec_helper'
-
-describe SlackService::IssueMessage, models: true do
- subject { SlackService::IssueMessage.new(args) }
-
- let(:args) do
- {
- user: {
- name: 'Test User',
- username: 'test.user'
- },
- project_name: 'project_name',
- project_url: 'somewhere.com',
-
- object_attributes: {
- title: 'Issue title',
- id: 10,
- iid: 100,
- assignee_id: 1,
- url: 'url',
- action: 'open',
- state: 'opened',
- description: 'issue description'
- }
- }
- end
-
- let(:color) { '#C95823' }
-
- context '#initialize' do
- before do
- args[:object_attributes][:description] = nil
- end
-
- it 'returns a non-null description' do
- expect(subject.description).to eq('')
- end
- end
-
- context 'open' do
- it 'returns a message regarding opening of issues' do
- expect(subject.pretext).to eq(
- '<somewhere.com|[project_name>] Issue opened by test.user')
- expect(subject.attachments).to eq([
- {
- title: "#100 Issue title",
- title_link: "url",
- text: "issue description",
- color: color,
- }
- ])
- end
- end
-
- context 'close' do
- before do
- args[:object_attributes][:action] = 'close'
- args[:object_attributes][:state] = 'closed'
- end
-
- it 'returns a message regarding closing of issues' do
- expect(subject.pretext). to eq(
- '<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by test.user')
- expect(subject.attachments).to be_empty
- end
- end
-end
diff --git a/spec/models/project_services/slack_service/merge_message_spec.rb b/spec/models/project_services/slack_service/merge_message_spec.rb
deleted file mode 100644
index c5c052d9af1..00000000000
--- a/spec/models/project_services/slack_service/merge_message_spec.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-require 'spec_helper'
-
-describe SlackService::MergeMessage, models: true do
- subject { SlackService::MergeMessage.new(args) }
-
- let(:args) do
- {
- user: {
- name: 'Test User',
- username: 'test.user'
- },
- project_name: 'project_name',
- project_url: 'somewhere.com',
-
- object_attributes: {
- title: "Issue title\nSecond line",
- id: 10,
- iid: 100,
- assignee_id: 1,
- url: 'url',
- state: 'opened',
- description: 'issue description',
- source_branch: 'source_branch',
- target_branch: 'target_branch',
- }
- }
- end
-
- let(:color) { '#345' }
-
- context 'open' do
- it 'returns a message regarding opening of merge requests' do
- expect(subject.pretext).to eq(
- 'test.user opened <somewhere.com/merge_requests/100|merge request !100> '\
- 'in <somewhere.com|project_name>: *Issue title*')
- expect(subject.attachments).to be_empty
- end
- end
-
- context 'close' do
- before do
- args[:object_attributes][:state] = 'closed'
- end
- it 'returns a message regarding closing of merge requests' do
- expect(subject.pretext).to eq(
- 'test.user closed <somewhere.com/merge_requests/100|merge request !100> '\
- 'in <somewhere.com|project_name>: *Issue title*')
- expect(subject.attachments).to be_empty
- end
- end
-end
diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb
deleted file mode 100644
index 38cfe4ad3e3..00000000000
--- a/spec/models/project_services/slack_service/note_message_spec.rb
+++ /dev/null
@@ -1,130 +0,0 @@
-require 'spec_helper'
-
-describe SlackService::NoteMessage, models: true do
- let(:color) { '#345' }
-
- before do
- @args = {
- user: {
- name: 'Test User',
- username: 'test.user',
- avatar_url: 'http://fakeavatar'
- },
- project_name: 'project_name',
- project_url: 'somewhere.com',
- repository: {
- name: 'project_name',
- url: 'somewhere.com',
- },
- object_attributes: {
- id: 10,
- note: 'comment on a commit',
- url: 'url',
- noteable_type: 'Commit'
- }
- }
- end
-
- context 'commit notes' do
- before do
- @args[:object_attributes][:note] = 'comment on a commit'
- @args[:object_attributes][:noteable_type] = 'Commit'
- @args[:commit] = {
- id: '5f163b2b95e6f53cbd428f5f0b103702a52b9a23',
- message: "Added a commit message\ndetails\n123\n"
- }
- end
-
- it 'returns a message regarding notes on commits' do
- message = SlackService::NoteMessage.new(@args)
- expect(message.pretext).to eq("test.user commented on " \
- "<url|commit 5f163b2b> in <somewhere.com|project_name>: " \
- "*Added a commit message*")
- expected_attachments = [
- {
- text: "comment on a commit",
- color: color,
- }
- ]
- expect(message.attachments).to eq(expected_attachments)
- end
- end
-
- context 'merge request notes' do
- before do
- @args[:object_attributes][:note] = 'comment on a merge request'
- @args[:object_attributes][:noteable_type] = 'MergeRequest'
- @args[:merge_request] = {
- id: 1,
- iid: 30,
- title: "merge request title\ndetails\n"
- }
- end
-
- it 'returns a message regarding notes on a merge request' do
- message = SlackService::NoteMessage.new(@args)
- expect(message.pretext).to eq("test.user commented on " \
- "<url|merge request !30> in <somewhere.com|project_name>: " \
- "*merge request title*")
- expected_attachments = [
- {
- text: "comment on a merge request",
- color: color,
- }
- ]
- expect(message.attachments).to eq(expected_attachments)
- end
- end
-
- context 'issue notes' do
- before do
- @args[:object_attributes][:note] = 'comment on an issue'
- @args[:object_attributes][:noteable_type] = 'Issue'
- @args[:issue] = {
- id: 1,
- iid: 20,
- title: "issue title\ndetails\n"
- }
- end
-
- it 'returns a message regarding notes on an issue' do
- message = SlackService::NoteMessage.new(@args)
- expect(message.pretext).to eq(
- "test.user commented on " \
- "<url|issue #20> in <somewhere.com|project_name>: " \
- "*issue title*")
- expected_attachments = [
- {
- text: "comment on an issue",
- color: color,
- }
- ]
- expect(message.attachments).to eq(expected_attachments)
- end
- end
-
- context 'project snippet notes' do
- before do
- @args[:object_attributes][:note] = 'comment on a snippet'
- @args[:object_attributes][:noteable_type] = 'Snippet'
- @args[:snippet] = {
- id: 5,
- title: "snippet title\ndetails\n"
- }
- end
-
- it 'returns a message regarding notes on a project snippet' do
- message = SlackService::NoteMessage.new(@args)
- expect(message.pretext).to eq("test.user commented on " \
- "<url|snippet #5> in <somewhere.com|project_name>: " \
- "*snippet title*")
- expected_attachments = [
- {
- text: "comment on a snippet",
- color: color,
- }
- ]
- expect(message.attachments).to eq(expected_attachments)
- end
- end
-end
diff --git a/spec/models/project_services/slack_service/pipeline_message_spec.rb b/spec/models/project_services/slack_service/pipeline_message_spec.rb
deleted file mode 100644
index babb3909f56..00000000000
--- a/spec/models/project_services/slack_service/pipeline_message_spec.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-require 'spec_helper'
-
-describe SlackService::PipelineMessage do
- subject { SlackService::PipelineMessage.new(args) }
-
- let(:args) do
- {
- object_attributes: {
- id: 123,
- sha: '97de212e80737a608d939f648d959671fb0a0142',
- tag: false,
- ref: 'develop',
- status: status,
- duration: duration
- },
- project: { path_with_namespace: 'project_name',
- web_url: 'example.gitlab.com' },
- commit: { author_name: 'hacker' }
- }
- end
-
- let(:message) { build_message }
-
- context 'pipeline succeeded' do
- let(:status) { 'success' }
- let(:color) { 'good' }
- let(:duration) { 10 }
- let(:message) { build_message('passed') }
-
- it 'returns a message with information about succeeded build' do
- expect(subject.pretext).to be_empty
- expect(subject.fallback).to eq(message)
- expect(subject.attachments).to eq([text: message, color: color])
- end
- end
-
- context 'pipeline failed' do
- let(:status) { 'failed' }
- let(:color) { 'danger' }
- let(:duration) { 10 }
-
- it 'returns a message with information about failed build' do
- expect(subject.pretext).to be_empty
- expect(subject.fallback).to eq(message)
- expect(subject.attachments).to eq([text: message, color: color])
- end
- end
-
- def build_message(status_text = status)
- "<example.gitlab.com|project_name>:" \
- " Pipeline <example.gitlab.com/pipelines/123|97de212e>" \
- " of <example.gitlab.com/commits/develop|develop> branch" \
- " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
- end
-end
diff --git a/spec/models/project_services/slack_service/push_message_spec.rb b/spec/models/project_services/slack_service/push_message_spec.rb
deleted file mode 100644
index 17cd05e24f1..00000000000
--- a/spec/models/project_services/slack_service/push_message_spec.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-require 'spec_helper'
-
-describe SlackService::PushMessage, models: true do
- subject { SlackService::PushMessage.new(args) }
-
- let(:args) do
- {
- after: 'after',
- before: 'before',
- project_name: 'project_name',
- ref: 'refs/heads/master',
- user_name: 'test.user',
- project_url: 'url'
- }
- end
-
- let(:color) { '#345' }
-
- context 'push' do
- before do
- args[:commits] = [
- { message: 'message1', url: 'url1', id: 'abcdefghijkl', author: { name: 'author1' } },
- { message: 'message2', url: 'url2', id: '123456789012', author: { name: 'author2' } },
- ]
- end
-
- it 'returns a message regarding pushes' do
- expect(subject.pretext).to eq(
- 'test.user pushed to branch <url/commits/master|master> of '\
- '<url|project_name> (<url/compare/before...after|Compare changes>)'
- )
- expect(subject.attachments).to eq([
- {
- text: "<url1|abcdefgh>: message1 - author1\n"\
- "<url2|12345678>: message2 - author2",
- color: color,
- }
- ])
- end
- end
-
- context 'tag push' do
- let(:args) do
- {
- after: 'after',
- before: Gitlab::Git::BLANK_SHA,
- project_name: 'project_name',
- ref: 'refs/tags/new_tag',
- user_name: 'test.user',
- project_url: 'url'
- }
- end
-
- it 'returns a message regarding pushes' do
- expect(subject.pretext).to eq('test.user pushed new tag ' \
- '<url/commits/new_tag|new_tag> to ' \
- '<url|project_name>')
- expect(subject.attachments).to be_empty
- end
- end
-
- context 'new branch' do
- before do
- args[:before] = Gitlab::Git::BLANK_SHA
- end
-
- it 'returns a message regarding a new branch' do
- expect(subject.pretext).to eq(
- 'test.user pushed new branch <url/commits/master|master> to '\
- '<url|project_name>'
- )
- expect(subject.attachments).to be_empty
- end
- end
-
- context 'removed branch' do
- before do
- args[:after] = Gitlab::Git::BLANK_SHA
- end
-
- it 'returns a message regarding a removed branch' do
- expect(subject.pretext).to eq(
- 'test.user removed branch master from <url|project_name>'
- )
- expect(subject.attachments).to be_empty
- end
- end
-end
diff --git a/spec/models/project_services/slack_service/wiki_page_message_spec.rb b/spec/models/project_services/slack_service/wiki_page_message_spec.rb
deleted file mode 100644
index 093911598b0..00000000000
--- a/spec/models/project_services/slack_service/wiki_page_message_spec.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-require 'spec_helper'
-
-describe SlackService::WikiPageMessage, models: true do
- subject { described_class.new(args) }
-
- let(:args) do
- {
- user: {
- name: 'Test User',
- username: 'test.user'
- },
- project_name: 'project_name',
- project_url: 'somewhere.com',
- object_attributes: {
- title: 'Wiki page title',
- url: 'url',
- content: 'Wiki page description'
- }
- }
- end
-
- describe '#pretext' do
- context 'when :action == "create"' do
- before { args[:object_attributes][:action] = 'create' }
-
- it 'returns a message that a new wiki page was created' do
- expect(subject.pretext).to eq(
- 'test.user created <url|wiki page> in <somewhere.com|project_name>: '\
- '*Wiki page title*')
- end
- end
-
- context 'when :action == "update"' do
- before { args[:object_attributes][:action] = 'update' }
-
- it 'returns a message that a wiki page was updated' do
- expect(subject.pretext).to eq(
- 'test.user edited <url|wiki page> in <somewhere.com|project_name>: '\
- '*Wiki page title*')
- end
- end
- end
-
- describe '#attachments' do
- let(:color) { '#345' }
-
- context 'when :action == "create"' do
- before { args[:object_attributes][:action] = 'create' }
-
- it 'returns the attachment for a new wiki page' do
- expect(subject.attachments).to eq([
- {
- text: "Wiki page description",
- color: color,
- }
- ])
- end
- end
-
- context 'when :action == "update"' do
- before { args[:object_attributes][:action] = 'update' }
-
- it 'returns the attachment for an updated wiki page' do
- expect(subject.attachments).to eq([
- {
- text: "Wiki page description",
- color: color,
- }
- ])
- end
- end
- end
-end
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb
deleted file mode 100644
index c07a70a8069..00000000000
--- a/spec/models/project_services/slack_service_spec.rb
+++ /dev/null
@@ -1,327 +0,0 @@
-require 'spec_helper'
-
-describe SlackService, models: true do
- let(:slack) { SlackService.new }
- let(:webhook_url) { 'https://example.gitlab.com/' }
-
- describe "Associations" do
- it { is_expected.to belong_to :project }
- it { is_expected.to have_one :service_hook }
- end
-
- describe 'Validations' do
- context 'when service is active' do
- before { subject.active = true }
-
- it { is_expected.to validate_presence_of(:webhook) }
- it_behaves_like 'issue tracker service URL attribute', :webhook
- end
-
- context 'when service is inactive' do
- before { subject.active = false }
-
- it { is_expected.not_to validate_presence_of(:webhook) }
- end
- end
-
- describe "Execute" do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:username) { 'slack_username' }
- let(:channel) { 'slack_channel' }
-
- let(:push_sample_data) do
- Gitlab::DataBuilder::Push.build_sample(project, user)
- end
-
- before do
- allow(slack).to receive_messages(
- project: project,
- project_id: project.id,
- service_hook: true,
- webhook: webhook_url
- )
-
- WebMock.stub_request(:post, webhook_url)
-
- opts = {
- title: 'Awesome issue',
- description: 'please fix'
- }
-
- issue_service = Issues::CreateService.new(project, user, opts)
- @issue = issue_service.execute
- @issues_sample_data = issue_service.hook_data(@issue, 'open')
-
- opts = {
- title: 'Awesome merge_request',
- description: 'please fix',
- source_branch: 'feature',
- target_branch: 'master'
- }
- merge_service = MergeRequests::CreateService.new(project,
- user, opts)
- @merge_request = merge_service.execute
- @merge_sample_data = merge_service.hook_data(@merge_request,
- 'open')
-
- opts = {
- title: "Awesome wiki_page",
- content: "Some text describing some thing or another",
- format: "md",
- message: "user created page: Awesome wiki_page"
- }
-
- wiki_page_service = WikiPages::CreateService.new(project, user, opts)
- @wiki_page = wiki_page_service.execute
- @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create')
- end
-
- it "calls Slack API for push events" do
- slack.execute(push_sample_data)
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
-
- it "calls Slack API for issue events" do
- slack.execute(@issues_sample_data)
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
-
- it "calls Slack API for merge requests events" do
- slack.execute(@merge_sample_data)
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
-
- it "calls Slack API for wiki page events" do
- slack.execute(@wiki_page_sample_data)
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
-
- it 'uses the username as an option for slack when configured' do
- allow(slack).to receive(:username).and_return(username)
- expect(Slack::Notifier).to receive(:new).
- with(webhook_url, username: username).
- and_return(
- double(:slack_service).as_null_object
- )
-
- slack.execute(push_sample_data)
- end
-
- it 'uses the channel as an option when it is configured' do
- allow(slack).to receive(:channel).and_return(channel)
- expect(Slack::Notifier).to receive(:new).
- with(webhook_url, channel: channel).
- and_return(
- double(:slack_service).as_null_object
- )
- slack.execute(push_sample_data)
- end
-
- context "event channels" do
- it "uses the right channel for push event" do
- slack.update_attributes(push_channel: "random")
-
- expect(Slack::Notifier).to receive(:new).
- with(webhook_url, channel: "random").
- and_return(
- double(:slack_service).as_null_object
- )
-
- slack.execute(push_sample_data)
- end
-
- it "uses the right channel for merge request event" do
- slack.update_attributes(merge_request_channel: "random")
-
- expect(Slack::Notifier).to receive(:new).
- with(webhook_url, channel: "random").
- and_return(
- double(:slack_service).as_null_object
- )
-
- slack.execute(@merge_sample_data)
- end
-
- it "uses the right channel for issue event" do
- slack.update_attributes(issue_channel: "random")
-
- expect(Slack::Notifier).to receive(:new).
- with(webhook_url, channel: "random").
- and_return(
- double(:slack_service).as_null_object
- )
-
- slack.execute(@issues_sample_data)
- end
-
- it "uses the right channel for wiki event" do
- slack.update_attributes(wiki_page_channel: "random")
-
- expect(Slack::Notifier).to receive(:new).
- with(webhook_url, channel: "random").
- and_return(
- double(:slack_service).as_null_object
- )
-
- slack.execute(@wiki_page_sample_data)
- end
-
- context "note event" do
- let(:issue_note) do
- create(:note_on_issue, project: project, note: "issue note")
- end
-
- it "uses the right channel" do
- slack.update_attributes(note_channel: "random")
-
- note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
-
- expect(Slack::Notifier).to receive(:new).
- with(webhook_url, channel: "random").
- and_return(
- double(:slack_service).as_null_object
- )
-
- slack.execute(note_data)
- end
- end
- end
- end
-
- describe "Note events" do
- let(:user) { create(:user) }
- let(:project) { create(:project, creator_id: user.id) }
-
- before do
- allow(slack).to receive_messages(
- project: project,
- project_id: project.id,
- service_hook: true,
- webhook: webhook_url
- )
-
- WebMock.stub_request(:post, webhook_url)
- end
-
- context 'when commit comment event executed' do
- let(:commit_note) do
- create(:note_on_commit, author: user,
- project: project,
- commit_id: project.repository.commit.id,
- note: 'a comment on a commit')
- end
-
- it "calls Slack API for commit comment events" do
- data = Gitlab::DataBuilder::Note.build(commit_note, user)
- slack.execute(data)
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
- end
-
- context 'when merge request comment event executed' do
- let(:merge_request_note) do
- create(:note_on_merge_request, project: project,
- note: "merge request note")
- end
-
- it "calls Slack API for merge request comment events" do
- data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
- slack.execute(data)
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
- end
-
- context 'when issue comment event executed' do
- let(:issue_note) do
- create(:note_on_issue, project: project, note: "issue note")
- end
-
- it "calls Slack API for issue comment events" do
- data = Gitlab::DataBuilder::Note.build(issue_note, user)
- slack.execute(data)
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
- end
-
- context 'when snippet comment event executed' do
- let(:snippet_note) do
- create(:note_on_project_snippet, project: project,
- note: "snippet note")
- end
-
- it "calls Slack API for snippet comment events" do
- data = Gitlab::DataBuilder::Note.build(snippet_note, user)
- slack.execute(data)
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
- end
- end
-
- describe 'Pipeline events' do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
-
- let(:pipeline) do
- create(:ci_pipeline,
- project: project, status: status,
- sha: project.commit.sha, ref: project.default_branch)
- end
-
- before do
- allow(slack).to receive_messages(
- project: project,
- service_hook: true,
- webhook: webhook_url
- )
- end
-
- shared_examples 'call Slack API' do
- before do
- WebMock.stub_request(:post, webhook_url)
- end
-
- it 'calls Slack API for pipeline events' do
- data = Gitlab::DataBuilder::Pipeline.build(pipeline)
- slack.execute(data)
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
- end
-
- context 'with failed pipeline' do
- let(:status) { 'failed' }
-
- it_behaves_like 'call Slack API'
- end
-
- context 'with succeeded pipeline' do
- let(:status) { 'success' }
-
- context 'with default to notify_only_broken_pipelines' do
- it 'does not call Slack API for pipeline events' do
- data = Gitlab::DataBuilder::Pipeline.build(pipeline)
- result = slack.execute(data)
-
- expect(result).to be_falsy
- end
- end
-
- context 'with setting notify_only_broken_pipelines to false' do
- before do
- slack.notify_only_broken_pipelines = false
- end
-
- it_behaves_like 'call Slack API'
- end
- end
- end
-end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 0810d06b50f..ed6b2c6a22b 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -20,8 +20,10 @@ describe Project, models: true do
it { is_expected.to have_many(:deploy_keys) }
it { is_expected.to have_many(:hooks).dependent(:destroy) }
it { is_expected.to have_many(:protected_branches).dependent(:destroy) }
+ it { is_expected.to have_many(:chat_services) }
it { is_expected.to have_one(:forked_project_link).dependent(:destroy) }
- it { is_expected.to have_one(:slack_service).dependent(:destroy) }
+ it { is_expected.to have_one(:slack_notification_service).dependent(:destroy) }
+ it { is_expected.to have_one(:mattermost_notification_service).dependent(:destroy) }
it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
it { is_expected.to have_one(:asana_service).dependent(:destroy) }
it { is_expected.to have_many(:boards).dependent(:destroy) }
@@ -35,6 +37,7 @@ describe Project, models: true do
it { is_expected.to have_one(:hipchat_service).dependent(:destroy) }
it { is_expected.to have_one(:flowdock_service).dependent(:destroy) }
it { is_expected.to have_one(:assembla_service).dependent(:destroy) }
+ it { is_expected.to have_one(:mattermost_slash_commands_service).dependent(:destroy) }
it { is_expected.to have_one(:gemnasium_service).dependent(:destroy) }
it { is_expected.to have_one(:buildkite_service).dependent(:destroy) }
it { is_expected.to have_one(:bamboo_service).dependent(:destroy) }
@@ -76,7 +79,7 @@ describe Project, models: true do
end
describe '#members & #requesters' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:requester) { create(:user) }
let(:developer) { create(:user) }
before do
@@ -129,14 +132,18 @@ describe Project, models: true do
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:namespace_id) }
- it { is_expected.to validate_length_of(:name).is_within(0..255) }
+ it { is_expected.to validate_length_of(:name).is_at_most(255) }
it { is_expected.to validate_presence_of(:path) }
it { is_expected.to validate_uniqueness_of(:path).scoped_to(:namespace_id) }
- it { is_expected.to validate_length_of(:path).is_within(0..255) }
- it { is_expected.to validate_length_of(:description).is_within(0..2000) }
+ it { is_expected.to validate_length_of(:path).is_at_most(255) }
+
+ it { is_expected.to validate_length_of(:description).is_at_most(2000) }
+
it { is_expected.to validate_presence_of(:creator) }
+
it { is_expected.to validate_presence_of(:namespace) }
+
it { is_expected.to validate_presence_of(:repository_storage) }
it 'does not allow new projects beyond user limits' do
@@ -241,6 +248,13 @@ describe Project, models: true do
it { is_expected.to respond_to(:path_with_namespace) }
end
+ describe 'delegation' do
+ it { is_expected.to delegate_method(:add_guest).to(:team) }
+ it { is_expected.to delegate_method(:add_reporter).to(:team) }
+ it { is_expected.to delegate_method(:add_developer).to(:team) }
+ it { is_expected.to delegate_method(:add_master).to(:team) }
+ end
+
describe '#name_with_namespace' do
let(:project) { build_stubbed(:empty_project) }
@@ -249,10 +263,70 @@ describe Project, models: true do
end
describe '#to_reference' do
- let(:project) { create(:empty_project) }
+ 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) }
+
+ context 'when nil argument' do
+ it 'returns nil' do
+ expect(project.to_reference).to be_nil
+ end
+ end
+
+ context 'when same project argument' do
+ it 'returns nil' do
+ expect(project.to_reference(project)).to be_nil
+ end
+ end
+
+ context 'when cross namespace project argument' do
+ let(:another_namespace_project) { create(:empty_project, name: 'another-project') }
+
+ it 'returns complete path to the project' do
+ expect(project.to_reference(another_namespace_project)).to eq 'sample-namespace/sample-project'
+ end
+ end
+
+ context 'when same namespace / cross-project argument' do
+ let(:another_project) { create(:empty_project, namespace: namespace) }
+
+ it 'returns complete path to the project' do
+ expect(project.to_reference(another_project)).to eq 'sample-project'
+ end
+ end
+ end
+
+ describe '#to_human_reference' do
+ let(:owner) { create(:user, name: 'Gitlab') }
+ let(:namespace) { create(:namespace, name: 'Sample namespace', owner: owner) }
+ let(:project) { create(:empty_project, name: 'Sample project', namespace: namespace) }
+
+ context 'when nil argument' do
+ it 'returns nil' do
+ expect(project.to_human_reference).to be_nil
+ end
+ end
+
+ context 'when same project argument' do
+ it 'returns nil' do
+ expect(project.to_human_reference(project)).to be_nil
+ end
+ end
+
+ context 'when cross namespace project argument' do
+ let(:another_namespace_project) { create(:empty_project, name: 'another-project') }
- it 'returns a String reference to the object' do
- expect(project.to_reference).to eq project.path_with_namespace
+ it 'returns complete name with namespace of the project' do
+ expect(project.to_human_reference(another_namespace_project)).to eq 'Gitlab / Sample project'
+ end
+ end
+
+ context 'when same namespace / cross-project argument' do
+ let(:another_project) { create(:empty_project, namespace: namespace) }
+
+ it 'returns name of the project' do
+ expect(project.to_human_reference(another_project)).to eq 'Sample project'
+ end
end
end
@@ -352,10 +426,15 @@ describe Project, models: true do
describe '#get_issue' do
let(:project) { create(:empty_project) }
let!(:issue) { create(:issue, project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :developer]
+ end
context 'with default issues tracker' do
it 'returns an issue' do
- expect(project.get_issue(issue.iid)).to eq issue
+ expect(project.get_issue(issue.iid, user)).to eq issue
end
it 'returns count of open issues' do
@@ -363,7 +442,12 @@ describe Project, models: true do
end
it 'returns nil when no issue found' do
- expect(project.get_issue(999)).to be_nil
+ expect(project.get_issue(999, user)).to be_nil
+ end
+
+ it "returns nil when user doesn't have access" do
+ user = create(:user)
+ expect(project.get_issue(issue.iid, user)).to eq nil
end
end
@@ -373,7 +457,7 @@ describe Project, models: true do
end
it 'returns an ExternalIssue' do
- issue = project.get_issue('FOO-1234')
+ issue = project.get_issue('FOO-1234', user)
expect(issue).to be_kind_of(ExternalIssue)
expect(issue.iid).to eq 'FOO-1234'
expect(issue.project).to eq project
@@ -395,35 +479,6 @@ describe Project, models: true do
end
end
- describe '.find_with_namespace' do
- context 'with namespace' do
- before do
- @group = create :group, name: 'gitlab'
- @project = create(:project, name: 'gitlabhq', namespace: @group)
- end
-
- it { expect(Project.find_with_namespace('gitlab/gitlabhq')).to eq(@project) }
- it { expect(Project.find_with_namespace('GitLab/GitlabHQ')).to eq(@project) }
- it { expect(Project.find_with_namespace('gitlab-ci')).to be_nil }
- end
-
- context 'when multiple projects using a similar name exist' do
- let(:group) { create(:group, name: 'gitlab') }
-
- let!(:project1) do
- create(:empty_project, name: 'gitlab1', path: 'gitlab', namespace: group)
- end
-
- let!(:project2) do
- create(:empty_project, name: 'gitlab2', path: 'GITLAB', namespace: group)
- end
-
- it 'returns the row where the path matches literally' do
- expect(Project.find_with_namespace('gitlab/GITLAB')).to eq(project2)
- end
- end
- end
-
describe '#to_param' do
context 'with namespace' do
before do
@@ -496,9 +551,6 @@ describe Project, models: true do
end
it 'returns nil and does not query services when there is no external issue tracker' do
- project.build_missing_services
- project.reload
-
expect(project).not_to receive(:services)
expect(project.external_issue_tracker).to eq(nil)
@@ -506,9 +558,6 @@ describe Project, models: true do
it 'retrieves external_issue_tracker querying services and cache it when there is external issue tracker' do
ext_project.reload # Factory returns a project with changed attributes
- ext_project.build_missing_services
- ext_project.reload
-
expect(ext_project).to receive(:services).once.and_call_original
2.times { expect(ext_project.external_issue_tracker).to be_a_kind_of(RedmineService) }
@@ -706,7 +755,7 @@ describe Project, models: true do
"/uploads/project/avatar/#{project.id}/uploads/avatar.png"
end
- it { should eq "http://localhost#{avatar_path}" }
+ it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" }
end
context 'When avatar file in git' do
@@ -718,7 +767,7 @@ describe Project, models: true do
"/#{project.namespace.name}/#{project.path}/avatar"
end
- it { should eq "http://localhost#{avatar_path}" }
+ it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" }
end
context 'when git repo is empty' do
@@ -1471,88 +1520,6 @@ describe Project, models: true do
end
end
- describe '.where_paths_in' do
- context 'without any paths' do
- it 'returns an empty relation' do
- expect(Project.where_paths_in([])).to eq([])
- end
- end
-
- context 'without any valid paths' do
- it 'returns an empty relation' do
- expect(Project.where_paths_in(%w[foo])).to eq([])
- end
- end
-
- context 'with valid paths' do
- let!(:project1) { create(:project) }
- let!(:project2) { create(:project) }
-
- it 'returns the projects matching the paths' do
- projects = Project.where_paths_in([project1.path_with_namespace,
- project2.path_with_namespace])
-
- expect(projects).to contain_exactly(project1, project2)
- end
-
- it 'returns projects regardless of the casing of paths' do
- projects = Project.where_paths_in([project1.path_with_namespace.upcase,
- project2.path_with_namespace.upcase])
-
- expect(projects).to contain_exactly(project1, project2)
- end
- end
- end
-
- describe 'authorized_for_user' do
- let(:group) { create(:group) }
- let(:developer) { create(:user) }
- let(:master) { create(:user) }
- let(:personal_project) { create(:project, namespace: developer.namespace) }
- let(:group_project) { create(:project, namespace: group) }
- let(:members_project) { create(:project) }
- let(:shared_project) { create(:project) }
-
- before do
- group.add_master(master)
- group.add_developer(developer)
-
- members_project.team << [developer, :developer]
- members_project.team << [master, :master]
-
- create(:project_group_link, project: shared_project, group: group)
- end
-
- it 'returns false for no user' do
- expect(personal_project.authorized_for_user?(nil)).to be(false)
- end
-
- it 'returns true for personal projects of the user' do
- expect(personal_project.authorized_for_user?(developer)).to be(true)
- end
-
- it 'returns true for projects of groups the user is a member of' do
- expect(group_project.authorized_for_user?(developer)).to be(true)
- end
-
- it 'returns true for projects for which the user is a member of' do
- expect(members_project.authorized_for_user?(developer)).to be(true)
- end
-
- it 'returns true for projects shared on a group the user is a member of' do
- expect(shared_project.authorized_for_user?(developer)).to be(true)
- end
-
- it 'checks for the correct minimum level access' do
- expect(group_project.authorized_for_user?(developer, Gitlab::Access::MASTER)).to be(false)
- expect(group_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true)
- expect(members_project.authorized_for_user?(developer, Gitlab::Access::MASTER)).to be(false)
- expect(members_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true)
- expect(shared_project.authorized_for_user?(developer, Gitlab::Access::MASTER)).to be(false)
- expect(shared_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true)
- end
- end
-
describe 'change_head' do
let(:project) { create(:project) }
@@ -1574,7 +1541,7 @@ describe Project, models: true do
end
it 'expires the avatar cache' do
- expect(project.repository).to receive(:expire_avatar_cache).with(project.default_branch)
+ expect(project.repository).to receive(:expire_avatar_cache)
project.change_head(project.default_branch)
end
@@ -1646,15 +1613,18 @@ describe Project, models: true do
end
it 'returns environment when with_tags is set' do
- expect(project.environments_for('master', project.commit, with_tags: true)).to contain_exactly(environment)
+ expect(project.environments_for('master', commit: project.commit, with_tags: true))
+ .to contain_exactly(environment)
end
it 'does not return environment when no with_tags is set' do
- expect(project.environments_for('master', project.commit)).to be_empty
+ expect(project.environments_for('master', commit: project.commit))
+ .to be_empty
end
it 'does not return environment when commit is not part of deployment' do
- expect(project.environments_for('master', project.commit('feature'))).to be_empty
+ expect(project.environments_for('master', commit: project.commit('feature')))
+ .to be_empty
end
end
@@ -1664,15 +1634,85 @@ describe Project, models: true do
end
it 'returns environment when ref is set' do
- expect(project.environments_for('master', project.commit)).to contain_exactly(environment)
+ expect(project.environments_for('master', commit: project.commit))
+ .to contain_exactly(environment)
end
it 'does not environment when ref is different' do
- expect(project.environments_for('feature', project.commit)).to be_empty
+ expect(project.environments_for('feature', commit: project.commit))
+ .to be_empty
end
it 'does not return environment when commit is not part of deployment' do
- expect(project.environments_for('master', project.commit('feature'))).to be_empty
+ expect(project.environments_for('master', commit: project.commit('feature')))
+ .to be_empty
+ end
+
+ it 'returns environment when commit constraint is not set' do
+ expect(project.environments_for('master'))
+ .to contain_exactly(environment)
+ end
+ end
+ end
+
+ describe '#environments_recently_updated_on_branch' do
+ let(:project) { create(:project) }
+ let(:environment) { create(:environment, project: project) }
+
+ context 'when last deployment to environment is the most recent one' do
+ before do
+ create(:deployment, environment: environment, ref: 'feature')
+ end
+
+ it 'finds recently updated environment' do
+ expect(project.environments_recently_updated_on_branch('feature'))
+ .to contain_exactly(environment)
+ end
+ end
+
+ context 'when last deployment to environment is not the most recent' do
+ before do
+ create(:deployment, environment: environment, ref: 'feature')
+ create(:deployment, environment: environment, ref: 'master')
+ end
+
+ it 'does not find environment' do
+ expect(project.environments_recently_updated_on_branch('feature'))
+ .to be_empty
+ end
+ end
+
+ context 'when there are two environments that deploy to the same branch' do
+ let(:second_environment) { create(:environment, project: project) }
+
+ before do
+ create(:deployment, environment: environment, ref: 'feature')
+ create(:deployment, environment: second_environment, ref: 'feature')
+ end
+
+ it 'finds both environments' do
+ expect(project.environments_recently_updated_on_branch('feature'))
+ .to contain_exactly(environment, second_environment)
+ end
+ end
+ end
+
+ describe '#deployment_variables' do
+ context 'when project has no deployment service' do
+ let(:project) { create(:empty_project) }
+
+ it 'returns an empty array' do
+ expect(project.deployment_variables).to eq []
+ end
+ end
+
+ context 'when project has a deployment service' do
+ let(:project) { create(:kubernetes_project) }
+
+ 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
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index e0f2dadf189..0475cecaa2d 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -10,9 +10,9 @@ describe ProjectTeam, models: true do
let(:project) { create(:empty_project) }
before do
- project.team << [master, :master]
- project.team << [reporter, :reporter]
- project.team << [guest, :guest]
+ project.add_master(master)
+ project.add_reporter(reporter)
+ project.add_guest(guest)
end
describe 'members collection' do
@@ -37,7 +37,7 @@ describe ProjectTeam, models: true do
context 'group project' do
let(:group) { create(:group) }
- let(:project) { create(:empty_project, group: group) }
+ let!(:project) { create(:empty_project, group: group) }
before do
group.add_master(master)
@@ -47,8 +47,8 @@ describe ProjectTeam, models: true do
# If user is a group and a project member - GitLab uses highest permission
# So we add group guest as master and add group master as guest
# to this project to test highest access
- project.team << [guest, :master]
- project.team << [master, :guest]
+ project.add_master(guest)
+ project.add_guest(master)
end
describe 'members collection' do
@@ -79,14 +79,14 @@ describe ProjectTeam, models: true do
it 'returns project members' do
user = create(:user)
- project.team << [user, :guest]
+ project.add_guest(user)
expect(project.team.members).to contain_exactly(user)
end
it 'returns project members of a specified level' do
user = create(:user)
- project.team << [user, :reporter]
+ project.add_reporter(user)
expect(project.team.guests).to be_empty
expect(project.team.reporters).to contain_exactly(user)
@@ -118,7 +118,7 @@ describe ProjectTeam, models: true do
context 'group project' do
let(:group) { create(:group) }
- let(:project) { create(:empty_project, group: group) }
+ let!(:project) { create(:empty_project, group: group) }
it 'returns project members' do
group_member = create(:group_member, group: group)
@@ -137,13 +137,13 @@ describe ProjectTeam, models: true do
describe '#find_member' do
context 'personal project' do
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:requester) { create(:user) }
before do
- project.team << [master, :master]
- project.team << [reporter, :reporter]
- project.team << [guest, :guest]
+ project.add_master(master)
+ project.add_reporter(reporter)
+ project.add_guest(guest)
project.request_access(requester)
end
@@ -155,7 +155,7 @@ describe ProjectTeam, models: true do
end
context 'group project' do
- let(:group) { create(:group) }
+ let(:group) { create(:group, :access_requestable) }
let(:project) { create(:empty_project, group: group) }
let(:requester) { create(:user) }
@@ -178,9 +178,9 @@ describe ProjectTeam, models: true do
it 'returns Master role' do
user = create(:user)
group = create(:group)
- group.add_master(user)
+ project = create(:empty_project, namespace: group)
- project = build_stubbed(:empty_project, namespace: group)
+ group.add_master(user)
expect(project.team.human_max_access(user.id)).to eq 'Master'
end
@@ -188,9 +188,9 @@ describe ProjectTeam, models: true do
it 'returns Owner role' do
user = create(:user)
group = create(:group)
- group.add_owner(user)
+ project = create(:empty_project, namespace: group)
- project = build_stubbed(:empty_project, namespace: group)
+ group.add_owner(user)
expect(project.team.human_max_access(user.id)).to eq 'Owner'
end
@@ -200,13 +200,13 @@ describe ProjectTeam, models: true do
let(:requester) { create(:user) }
context 'personal project' do
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
context 'when project is not shared with group' do
before do
- project.team << [master, :master]
- project.team << [reporter, :reporter]
- project.team << [guest, :guest]
+ project.add_master(master)
+ project.add_reporter(reporter)
+ project.add_guest(guest)
project.request_access(requester)
end
@@ -243,8 +243,8 @@ describe ProjectTeam, models: true do
end
context 'group project' do
- let(:group) { create(:group) }
- let(:project) { create(:empty_project, group: group) }
+ let(:group) { create(:group, :access_requestable) }
+ let!(:project) { create(:empty_project, group: group) }
before do
group.add_master(master)
@@ -261,6 +261,57 @@ describe ProjectTeam, models: true do
end
end
+ describe '#member?' do
+ let(:group) { create(:group) }
+ let(:developer) { create(:user) }
+ let(:master) { create(:user) }
+ let(:personal_project) { create(:project, namespace: developer.namespace) }
+ let(:group_project) { create(:project, namespace: group) }
+ let(:members_project) { create(:project) }
+ let(:shared_project) { create(:project) }
+
+ before do
+ group.add_master(master)
+ group.add_developer(developer)
+
+ members_project.team << [developer, :developer]
+ members_project.team << [master, :master]
+
+ create(:project_group_link, project: shared_project, group: group)
+ end
+
+ it 'returns false for no user' do
+ expect(personal_project.team.member?(nil)).to be(false)
+ end
+
+ it 'returns true for personal projects of the user' do
+ expect(personal_project.team.member?(developer)).to be(true)
+ end
+
+ it 'returns true for projects of groups the user is a member of' do
+ expect(group_project.team.member?(developer)).to be(true)
+ end
+
+ it 'returns true for projects for which the user is a member of' do
+ expect(members_project.team.member?(developer)).to be(true)
+ end
+
+ it 'returns true for projects shared on a group the user is a member of' do
+ expect(shared_project.team.member?(developer)).to be(true)
+ end
+
+ it 'checks for the correct minimum level access' do
+ expect(group_project.team.member?(developer, Gitlab::Access::MASTER)).to be(false)
+ expect(group_project.team.member?(master, Gitlab::Access::MASTER)).to be(true)
+ expect(members_project.team.member?(developer, Gitlab::Access::MASTER)).to be(false)
+ expect(members_project.team.member?(master, Gitlab::Access::MASTER)).to be(true)
+ expect(shared_project.team.member?(developer, Gitlab::Access::MASTER)).to be(false)
+ expect(shared_project.team.member?(master, Gitlab::Access::MASTER)).to be(false)
+ expect(shared_project.team.member?(developer, Gitlab::Access::DEVELOPER)).to be(true)
+ expect(shared_project.team.member?(master, Gitlab::Access::DEVELOPER)).to be(true)
+ end
+ end
+
shared_examples_for "#max_member_access_for_users" do |enable_request_store|
describe "#max_member_access_for_users" do
before do
@@ -281,10 +332,10 @@ describe ProjectTeam, models: true do
guest = create(:user)
project = create(:project)
- project.team << [master, :master]
- project.team << [reporter, :reporter]
- project.team << [promoted_guest, :guest]
- project.team << [guest, :guest]
+ project.add_master(master)
+ project.add_reporter(reporter)
+ project.add_guest(promoted_guest)
+ project.add_guest(guest)
group = create(:group)
group_developer = create(:user)
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 04b7d19d414..b5a42edd192 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -113,6 +113,26 @@ describe Repository, models: true do
end
end
+ describe '#ref_exists?' do
+ context 'when ref exists' do
+ it 'returns true' do
+ expect(repository.ref_exists?('refs/heads/master')).to be true
+ end
+ end
+
+ context 'when ref does not exist' do
+ it 'returns false' do
+ expect(repository.ref_exists?('refs/heads/non-existent')).to be false
+ end
+ end
+
+ context 'when ref format is incorrect' do
+ it 'returns false' do
+ expect(repository.ref_exists?('refs/heads/invalid:master')).to be false
+ end
+ end
+ end
+
describe '#last_commit_for_path' do
subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id }
@@ -197,6 +217,35 @@ describe Repository, models: true do
end
end
+ describe '#commit' do
+ context 'when ref exists' do
+ it 'returns commit object' do
+ expect(repository.commit('master'))
+ .to be_an_instance_of Commit
+ end
+ end
+
+ context 'when ref does not exist' do
+ it 'returns nil' do
+ expect(repository.commit('non-existent-ref')).to be_nil
+ end
+ end
+
+ context 'when ref is not valid' do
+ context 'when preceding tree element exists' do
+ it 'returns nil' do
+ expect(repository.commit('master:ref')).to be_nil
+ end
+ end
+
+ context 'when preceding tree element does not exist' do
+ it 'returns nil' do
+ expect(repository.commit('non-existent:ref')).to be_nil
+ end
+ end
+ end
+ end
+
describe "#commit_dir" do
it "commits a change that creates a new directory" do
expect do
@@ -344,24 +393,37 @@ describe Repository, models: true do
end
end
- describe "search_files" do
- let(:results) { repository.search_files('feature', 'master') }
+ describe "search_files_by_content" do
+ let(:results) { repository.search_files_by_content('feature', 'master') }
subject { results }
it { is_expected.to be_an Array }
it 'regex-escapes the query string' do
- results = repository.search_files("test\\", 'master')
+ results = repository.search_files_by_content("test\\", 'master')
expect(results.first).not_to start_with('fatal:')
end
it 'properly handles an unmatched parenthesis' do
- results = repository.search_files("test(", 'master')
+ results = repository.search_files_by_content("test(", 'master')
expect(results.first).not_to start_with('fatal:')
end
+ it 'properly handles when query is not present' do
+ results = repository.search_files_by_content('', 'master')
+
+ expect(results).to match_array([])
+ end
+
+ it 'properly handles query when repo is empty' do
+ repository = create(:empty_project).repository
+ results = repository.search_files_by_content('test', 'master')
+
+ expect(results).to match_array([])
+ end
+
describe 'result' do
subject { results.first }
@@ -370,6 +432,28 @@ describe Repository, models: true do
end
end
+ describe "search_files_by_name" do
+ let(:results) { repository.search_files_by_name('files', 'master') }
+
+ it 'returns result' do
+ expect(results.first).to eq('files/html/500.html')
+ end
+
+ it 'properly handles when query is not present' do
+ results = repository.search_files_by_name('', 'master')
+
+ expect(results).to match_array([])
+ end
+
+ it 'properly handles query when repo is empty' do
+ repository = create(:empty_project).repository
+
+ results = repository.search_files_by_name('test', 'master')
+
+ expect(results).to match_array([])
+ end
+ end
+
describe '#create_ref' do
it 'redirects the call to fetch_ref' do
ref, ref_path = '1', '2'
@@ -380,11 +464,7 @@ describe Repository, models: true do
end
end
- describe "#changelog" do
- before do
- repository.send(:cache).expire(:changelog)
- end
-
+ describe "#changelog", caching: true do
it 'accepts changelog' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changelog')])
@@ -416,17 +496,16 @@ describe Repository, models: true do
end
end
- describe "#license_blob" do
+ describe "#license_blob", caching: true do
before do
- repository.send(:cache).expire(:license_blob)
repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master')
end
it 'handles when HEAD points to non-existent ref' do
repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false)
- rugged = double('rugged')
- expect(rugged).to receive(:head_unborn?).and_return(true)
- expect(repository).to receive(:rugged).and_return(rugged)
+
+ allow(repository).to receive(:file_on_head).
+ and_raise(Rugged::ReferenceError)
expect(repository.license_blob).to be_nil
end
@@ -453,22 +532,18 @@ describe Repository, models: true do
end
end
- describe '#license_key' do
+ describe '#license_key', caching: true do
before do
- repository.send(:cache).expire(:license_key)
repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master')
end
- it 'handles when HEAD points to non-existent ref' do
- repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false)
- rugged = double('rugged')
- expect(rugged).to receive(:head_unborn?).and_return(true)
- expect(repository).to receive(:rugged).and_return(rugged)
-
+ it 'returns nil when no license is detected' do
expect(repository.license_key).to be_nil
end
- it 'returns nil when no license is detected' do
+ it 'returns nil when the repository does not exist' do
+ expect(repository).to receive(:exists?).and_return(false)
+
expect(repository.license_key).to be_nil
end
@@ -485,7 +560,7 @@ describe Repository, models: true do
end
end
- describe "#gitlab_ci_yml" do
+ describe "#gitlab_ci_yml", caching: true do
it 'returns valid file' do
files = [TestBlob.new('file'), TestBlob.new('.gitlab-ci.yml'), TestBlob.new('copying')]
expect(repository.tree).to receive(:blobs).and_return(files)
@@ -499,7 +574,7 @@ describe Repository, models: true do
end
it 'returns nil for empty repository' do
- expect(repository).to receive(:empty?).and_return(true)
+ allow(repository).to receive(:file_on_head).and_raise(Rugged::ReferenceError)
expect(repository.gitlab_ci_yml).to be_nil
end
end
@@ -693,8 +768,6 @@ describe Repository, models: true do
expect(repository).not_to receive(:expire_root_ref_cache)
expect(repository).not_to receive(:expire_emptiness_caches)
expect(repository).to receive(:expire_branches_cache)
- expect(repository).to receive(:expire_has_visible_content_cache)
- expect(repository).to receive(:expire_branch_count_cache)
repository.update_branch_with_hooks(user, 'new-feature') { new_rev }
end
@@ -712,8 +785,6 @@ describe Repository, models: true do
expect(empty_repository).to receive(:expire_root_ref_cache)
expect(empty_repository).to receive(:expire_emptiness_caches)
expect(empty_repository).to receive(:expire_branches_cache)
- expect(empty_repository).to receive(:expire_has_visible_content_cache)
- expect(empty_repository).to receive(:expire_branch_count_cache)
empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!',
'Updates file content', 'master', false)
@@ -727,8 +798,7 @@ describe Repository, models: true do
end
it 'returns false when a repository does not exist' do
- expect(repository.raw_repository).to receive(:rugged).
- and_raise(Gitlab::Git::Repository::NoRepository)
+ allow(repository).to receive(:refs_directory_exists?).and_return(false)
expect(repository.exists?).to eq(false)
end
@@ -757,15 +827,6 @@ describe Repository, models: true do
expect(subject).to eq(true)
end
-
- it 'caches the output' do
- expect(repository).to receive(:branch_count).
- once.
- and_return(3)
-
- repository.has_visible_content?
- repository.has_visible_content?
- end
end
end
@@ -832,34 +893,6 @@ describe Repository, models: true do
end
end
- describe '#expire_cache' do
- it 'expires all caches' do
- expect(repository).to receive(:expire_branch_cache)
-
- repository.expire_cache
- end
-
- it 'expires the caches for a specific branch' do
- expect(repository).to receive(:expire_branch_cache).with('master')
-
- repository.expire_cache('master')
- end
-
- it 'expires the emptiness caches for an empty repository' do
- expect(repository).to receive(:empty?).and_return(true)
- expect(repository).to receive(:expire_emptiness_caches)
-
- repository.expire_cache
- end
-
- it 'does not expire the emptiness caches for a non-empty repository' do
- expect(repository).to receive(:empty?).and_return(false)
- expect(repository).not_to receive(:expire_emptiness_caches)
-
- repository.expire_cache
- end
- end
-
describe '#expire_root_ref_cache' do
it 'expires the root reference cache' do
repository.root_ref
@@ -874,20 +907,6 @@ describe Repository, models: true do
end
end
- describe '#expire_has_visible_content_cache' do
- it 'expires the visible content cache' do
- repository.has_visible_content?
-
- expect(repository).to receive(:branch_count).
- once.
- and_return(0)
-
- repository.expire_has_visible_content_cache
-
- expect(repository.has_visible_content?).to eq(false)
- end
- end
-
describe '#expire_branch_cache' do
# This method is private but we need it for testing purposes. Sadly there's
# no other proper way of testing caching operations.
@@ -919,9 +938,18 @@ describe Repository, models: true do
describe '#expire_emptiness_caches' do
let(:cache) { repository.send(:cache) }
- it 'expires the caches' do
+ it 'expires the caches for an empty repository' do
+ allow(repository).to receive(:empty?).and_return(true)
+
expect(cache).to receive(:expire).with(:empty?)
- expect(repository).to receive(:expire_has_visible_content_cache)
+
+ repository.expire_emptiness_caches
+ end
+
+ it 'does not expire the cache for a non-empty repository' do
+ allow(repository).to receive(:empty?).and_return(false)
+
+ expect(cache).not_to receive(:expire).with(:empty?)
repository.expire_emptiness_caches
end
@@ -1036,24 +1064,12 @@ describe Repository, models: true do
repository.before_delete
end
- it 'flushes the tag count cache' do
- expect(repository).to receive(:expire_tag_count_cache)
-
- repository.before_delete
- end
-
it 'flushes the branches cache' do
expect(repository).to receive(:expire_branches_cache)
repository.before_delete
end
- it 'flushes the branch count cache' do
- expect(repository).to receive(:expire_branch_count_cache)
-
- repository.before_delete
- end
-
it 'flushes the root ref cache' do
expect(repository).to receive(:expire_root_ref_cache)
@@ -1078,36 +1094,18 @@ describe Repository, models: true do
allow(repository).to receive(:exists?).and_return(true)
end
- it 'flushes the caches that depend on repository data' do
- expect(repository).to receive(:expire_cache)
-
- repository.before_delete
- end
-
it 'flushes the tags cache' do
expect(repository).to receive(:expire_tags_cache)
repository.before_delete
end
- it 'flushes the tag count cache' do
- expect(repository).to receive(:expire_tag_count_cache)
-
- repository.before_delete
- end
-
it 'flushes the branches cache' do
expect(repository).to receive(:expire_branches_cache)
repository.before_delete
end
- it 'flushes the branch count cache' do
- expect(repository).to receive(:expire_branch_count_cache)
-
- repository.before_delete
- end
-
it 'flushes the root ref cache' do
expect(repository).to receive(:expire_root_ref_cache)
@@ -1138,8 +1136,9 @@ describe Repository, models: true do
describe '#before_push_tag' do
it 'flushes the cache' do
- expect(repository).to receive(:expire_cache)
- expect(repository).to receive(:expire_tag_count_cache)
+ expect(repository).to receive(:expire_statistics_caches)
+ expect(repository).to receive(:expire_emptiness_caches)
+ expect(repository).to receive(:expire_tags_cache)
repository.before_push_tag
end
@@ -1156,31 +1155,37 @@ describe Repository, models: true do
describe '#after_import' do
it 'flushes and builds the cache' do
expect(repository).to receive(:expire_content_cache)
- expect(repository).to receive(:build_cache)
+ expect(repository).to receive(:expire_tags_cache)
+ expect(repository).to receive(:expire_branches_cache)
repository.after_import
end
end
describe '#after_push_commit' do
- it 'flushes the cache' do
- expect(repository).to receive(:expire_cache).with('master', '123')
+ it 'expires statistics caches' do
+ expect(repository).to receive(:expire_statistics_caches).
+ and_call_original
- repository.after_push_commit('master', '123')
+ expect(repository).to receive(:expire_branch_cache).
+ with('master').
+ and_call_original
+
+ repository.after_push_commit('master')
end
end
describe '#after_create_branch' do
- it 'flushes the visible content cache' do
- expect(repository).to receive(:expire_has_visible_content_cache)
+ it 'expires the branch caches' do
+ expect(repository).to receive(:expire_branches_cache)
repository.after_create_branch
end
end
describe '#after_remove_branch' do
- it 'flushes the visible content cache' do
- expect(repository).to receive(:expire_has_visible_content_cache)
+ it 'expires the branch caches' do
+ expect(repository).to receive(:expire_branches_cache)
repository.after_remove_branch
end
@@ -1218,7 +1223,8 @@ describe Repository, models: true do
describe '#before_remove_tag' do
it 'flushes the tag cache' do
- expect(repository).to receive(:expire_tag_count_cache)
+ expect(repository).to receive(:expire_tags_cache).and_call_original
+ expect(repository).to receive(:expire_statistics_caches).and_call_original
repository.before_remove_tag
end
@@ -1236,23 +1242,23 @@ describe Repository, models: true do
end
end
- describe '#expire_branch_count_cache' do
- let(:cache) { repository.send(:cache) }
-
+ describe '#expire_branches_cache' do
it 'expires the cache' do
- expect(cache).to receive(:expire).with(:branch_count)
+ expect(repository).to receive(:expire_method_caches).
+ with(%i(branch_names branch_count)).
+ and_call_original
- repository.expire_branch_count_cache
+ repository.expire_branches_cache
end
end
- describe '#expire_tag_count_cache' do
- let(:cache) { repository.send(:cache) }
-
+ describe '#expire_tags_cache' do
it 'expires the cache' do
- expect(cache).to receive(:expire).with(:tag_count)
+ expect(repository).to receive(:expire_method_caches).
+ with(%i(tag_names tag_count)).
+ and_call_original
- repository.expire_tag_count_cache
+ repository.expire_tags_cache
end
end
@@ -1275,6 +1281,32 @@ describe Repository, models: true do
expect(tag).to be_a(Gitlab::Git::Tag)
end
+
+ it 'passes commit SHA to pre-receive and update hooks,\
+ and tag SHA to post-receive hook' do
+ pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', repository.path_to_repo)
+ update_hook = Gitlab::Git::Hook.new('update', repository.path_to_repo)
+ post_receive_hook = Gitlab::Git::Hook.new('post-receive', repository.path_to_repo)
+
+ allow(Gitlab::Git::Hook).to receive(:new).
+ and_return(pre_receive_hook, update_hook, post_receive_hook)
+
+ allow(pre_receive_hook).to receive(:trigger).and_call_original
+ allow(update_hook).to receive(:trigger).and_call_original
+ allow(post_receive_hook).to receive(:trigger).and_call_original
+
+ tag = repository.add_tag(user, '8.5', 'master', 'foo')
+
+ commit_sha = repository.commit('master').id
+ tag_sha = tag.target
+
+ expect(pre_receive_hook).to have_received(:trigger).
+ with(anything, anything, commit_sha, anything)
+ expect(update_hook).to have_received(:trigger).
+ with(anything, anything, commit_sha, anything)
+ expect(post_receive_hook).to have_received(:trigger).
+ with(anything, anything, tag_sha, anything)
+ end
end
context 'with an invalid target' do
@@ -1306,180 +1338,316 @@ describe Repository, models: true do
describe '#avatar' do
it 'returns nil if repo does not exist' do
- expect(repository).to receive(:exists?).and_return(false)
+ expect(repository).to receive(:file_on_head).
+ and_raise(Rugged::ReferenceError)
expect(repository.avatar).to eq(nil)
end
it 'returns the first avatar file found in the repository' do
- expect(repository).to receive(:blob_at_branch).
- with('master', 'logo.png').
- and_return(true)
+ expect(repository).to receive(:file_on_head).
+ with(:avatar).
+ and_return(double(:tree, path: 'logo.png'))
expect(repository.avatar).to eq('logo.png')
end
it 'caches the output' do
- allow(repository).to receive(:blob_at_branch).
- with('master', 'logo.png').
- and_return(true)
-
- expect(repository.avatar).to eq('logo.png')
+ expect(repository).to receive(:file_on_head).
+ with(:avatar).
+ once.
+ and_return(double(:tree, path: 'logo.png'))
- expect(repository).not_to receive(:blob_at_branch)
- expect(repository.avatar).to eq('logo.png')
+ 2.times { expect(repository.avatar).to eq('logo.png') }
end
end
- describe '#expire_avatar_cache' do
+ describe '#expire_exists_cache' do
let(:cache) { repository.send(:cache) }
- before do
- allow(repository).to receive(:cache).and_return(cache)
+ it 'expires the cache' do
+ expect(cache).to receive(:expire).with(:exists?)
+
+ repository.expire_exists_cache
end
+ end
- context 'without a branch or revision' do
- it 'flushes the cache' do
- expect(cache).to receive(:expire).with(:avatar)
+ describe "#keep_around" do
+ it "does not fail if we attempt to reference bad commit" do
+ expect(repository.kept_around?('abc1234')).to be_falsey
+ end
- repository.expire_avatar_cache
- end
+ it "stores a reference to the specified commit sha so it isn't garbage collected" do
+ repository.keep_around(sample_commit.id)
+
+ expect(repository.kept_around?(sample_commit.id)).to be_truthy
end
- context 'with a branch' do
- it 'does not flush the cache if the branch is not the default branch' do
- expect(cache).not_to receive(:expire)
+ it "attempting to call keep_around on truncated ref does not fail" do
+ repository.keep_around(sample_commit.id)
+ ref = repository.send(:keep_around_ref_name, sample_commit.id)
+ path = File.join(repository.path, ref)
+ # Corrupt the reference
+ File.truncate(path, 0)
+
+ expect(repository.kept_around?(sample_commit.id)).to be_falsey
+
+ repository.keep_around(sample_commit.id)
- repository.expire_avatar_cache('cats')
+ expect(repository.kept_around?(sample_commit.id)).to be_falsey
+
+ File.delete(path)
+ end
+ end
+
+ describe '#update_ref!' do
+ it 'can create a ref' do
+ repository.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)
+ end.to raise_error(Repository::CommitError)
+ end
+ end
+
+ describe '#contribution_guide', caching: true do
+ it 'returns and caches the output' do
+ expect(repository).to receive(:file_on_head).
+ with(:contributing).
+ and_return(Gitlab::Git::Tree.new(path: 'CONTRIBUTING.md')).
+ once
+
+ 2.times do
+ expect(repository.contribution_guide).
+ to be_an_instance_of(Gitlab::Git::Tree)
end
+ end
+ end
- it 'flushes the cache if the branch equals the default branch' do
- expect(cache).to receive(:expire).with(:avatar)
+ describe '#gitignore', caching: true do
+ it 'returns and caches the output' do
+ expect(repository).to receive(:file_on_head).
+ with(:gitignore).
+ and_return(Gitlab::Git::Tree.new(path: '.gitignore')).
+ once
- repository.expire_avatar_cache(repository.root_ref)
+ 2.times do
+ expect(repository.gitignore).to be_an_instance_of(Gitlab::Git::Tree)
end
end
+ end
- context 'with a branch and revision' do
- let(:commit) { double(:commit) }
+ describe '#koding_yml', caching: true do
+ it 'returns and caches the output' do
+ expect(repository).to receive(:file_on_head).
+ with(:koding).
+ and_return(Gitlab::Git::Tree.new(path: '.koding.yml')).
+ once
- before do
- allow(repository).to receive(:commit).and_return(commit)
+ 2.times do
+ expect(repository.koding_yml).to be_an_instance_of(Gitlab::Git::Tree)
end
+ end
+ end
- it 'does not flush the cache if the commit does not change any logos' do
- diff = double(:diff, new_path: 'test.txt')
+ describe '#readme', caching: true do
+ context 'with a non-existing repository' do
+ it 'returns nil' do
+ expect(repository).to receive(:tree).with(:head).and_return(nil)
- expect(commit).to receive(:raw_diffs).and_return([diff])
- expect(cache).not_to receive(:expire)
+ expect(repository.readme).to be_nil
+ end
+ end
- repository.expire_avatar_cache(repository.root_ref, '123')
+ context 'with an existing repository' do
+ it 'returns the README' do
+ expect(repository.readme).to be_an_instance_of(Gitlab::Git::Blob)
end
+ end
+ end
- it 'flushes the cache if the commit changes any of the logos' do
- diff = double(:diff, new_path: Repository::AVATAR_FILES[0])
+ describe '#expire_statistics_caches' do
+ it 'expires the caches' do
+ expect(repository).to receive(:expire_method_caches).
+ with(%i(size commit_count))
- expect(commit).to receive(:raw_diffs).and_return([diff])
- expect(cache).to receive(:expire).with(:avatar)
+ repository.expire_statistics_caches
+ end
+ end
- repository.expire_avatar_cache(repository.root_ref, '123')
- end
+ describe '#expire_method_caches' do
+ it 'expires the caches of the given methods' do
+ expect_any_instance_of(RepositoryCache).to receive(:expire).with(:readme)
+ expect_any_instance_of(RepositoryCache).to receive(:expire).with(:gitignore)
+
+ repository.expire_method_caches(%i(readme gitignore))
end
end
- describe '#expire_exists_cache' do
- let(:cache) { repository.send(:cache) }
+ describe '#expire_all_method_caches' do
+ it 'expires the caches of all methods' do
+ expect(repository).to receive(:expire_method_caches).
+ with(Repository::CACHED_METHODS)
+ repository.expire_all_method_caches
+ end
+ end
+
+ describe '#expire_avatar_cache' do
it 'expires the cache' do
- expect(cache).to receive(:expire).with(:exists?)
+ expect(repository).to receive(:expire_method_caches).with(%i(avatar))
- repository.expire_exists_cache
+ repository.expire_avatar_cache
end
end
- describe '#build_cache' do
- let(:cache) { repository.send(:cache) }
+ describe '#file_on_head' do
+ context 'with a non-existing repository' do
+ it 'returns nil' do
+ expect(repository).to receive(:tree).with(:head).and_return(nil)
- it 'builds the caches if they do not already exist' do
- cache_keys = repository.cache_keys + repository.cache_keys_for_branches_and_tags
+ expect(repository.file_on_head(:readme)).to be_nil
+ end
+ end
- expect(cache).to receive(:exist?).
- exactly(cache_keys.length).
- times.
- and_return(false)
+ context 'with a repository that has no blobs' do
+ it 'returns nil' do
+ expect_any_instance_of(Tree).to receive(:blobs).and_return([])
- cache_keys.each do |key|
- expect(repository).to receive(key)
+ expect(repository.file_on_head(:readme)).to be_nil
end
+ end
- repository.build_cache
+ context 'with an existing repository' do
+ it 'returns a Gitlab::Git::Tree' do
+ expect(repository.file_on_head(:readme)).
+ to be_an_instance_of(Gitlab::Git::Tree)
+ end
end
+ end
- it 'does not build any caches that already exist' do
- cache_keys = repository.cache_keys + repository.cache_keys_for_branches_and_tags
+ describe '#head_tree' do
+ context 'with an existing repository' do
+ it 'returns a Tree' do
+ expect(repository.head_tree).to be_an_instance_of(Tree)
+ end
+ end
- expect(cache).to receive(:exist?).
- exactly(cache_keys.length).
- times.
- and_return(true)
+ context 'with a non-existing repository' do
+ it 'returns nil' do
+ expect(repository).to receive(:head_commit).and_return(nil)
- cache_keys.each do |key|
- expect(repository).not_to receive(key)
+ expect(repository.head_tree).to be_nil
end
-
- repository.build_cache
end
end
- describe "#keep_around" do
- it "does not fail if we attempt to reference bad commit" do
- expect(repository.kept_around?('abc1234')).to be_falsey
- end
+ describe '#tree' do
+ context 'using a non-existing repository' do
+ before do
+ allow(repository).to receive(:head_commit).and_return(nil)
+ end
- it "stores a reference to the specified commit sha so it isn't garbage collected" do
- repository.keep_around(sample_commit.id)
+ it 'returns nil' do
+ expect(repository.tree(:head)).to be_nil
+ end
- expect(repository.kept_around?(sample_commit.id)).to be_truthy
+ it 'returns nil when using a path' do
+ expect(repository.tree(:head, 'README.md')).to be_nil
+ end
end
- it "attempting to call keep_around on truncated ref does not fail" do
- repository.keep_around(sample_commit.id)
- ref = repository.send(:keep_around_ref_name, sample_commit.id)
- path = File.join(repository.path, ref)
- # Corrupt the reference
- File.truncate(path, 0)
+ context 'using an existing repository' do
+ it 'returns a Tree' do
+ expect(repository.tree(:head)).to be_an_instance_of(Tree)
+ end
+ end
+ end
- expect(repository.kept_around?(sample_commit.id)).to be_falsey
+ describe '#size' do
+ context 'with a non-existing repository' do
+ it 'returns 0' do
+ expect(repository).to receive(:exists?).and_return(false)
- repository.keep_around(sample_commit.id)
+ expect(repository.size).to eq(0.0)
+ end
+ end
- expect(repository.kept_around?(sample_commit.id)).to be_falsey
+ context 'with an existing repository' do
+ it 'returns the repository size as a Float' do
+ expect(repository.size).to be_an_instance_of(Float)
+ end
+ end
+ end
- File.delete(path)
+ describe '#commit_count' do
+ context 'with a non-existing repository' do
+ it 'returns 0' do
+ expect(repository).to receive(:root_ref).and_return(nil)
+
+ expect(repository.commit_count).to eq(0)
+ end
+ end
+
+ context 'with an existing repository' do
+ it 'returns the commit count' do
+ expect(repository.commit_count).to be_an_instance_of(Fixnum)
+ end
end
end
- describe '#update_ref!' do
- it 'can create a ref' do
- repository.update_ref!('refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
+ describe '#cache_method_output', caching: true do
+ context 'with a non-existing repository' do
+ let(:value) do
+ repository.cache_method_output(:cats, fallback: 10) do
+ raise Rugged::ReferenceError
+ end
+ end
- expect(repository.find_branch('foobar')).not_to be_nil
+ it 'returns a fallback value' do
+ expect(value).to eq(10)
+ end
+
+ it 'does not cache the data' do
+ value
+
+ expect(repository.instance_variable_defined?(:@cats)).to eq(false)
+ expect(repository.send(:cache).exist?(:cats)).to eq(false)
+ end
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)
- end.to raise_error(Repository::CommitError)
+ context 'with an existing repository' do
+ it 'caches the output' do
+ object = double
+
+ expect(object).to receive(:number).once.and_return(10)
+
+ 2.times do
+ val = repository.cache_method_output(:cats) { object.number }
+
+ expect(val).to eq(10)
+ end
+
+ expect(repository.send(:cache).exist?(:cats)).to eq(true)
+ expect(repository.instance_variable_get(:@cats)).to eq(10)
+ end
end
end
- describe '#remove_storage_from_path' do
- let(:storage_path) { project.repository_storage_path }
- let(:project_path) { project.path_with_namespace }
- let(:full_path) { File.join(storage_path, project_path) }
+ describe '#refresh_method_caches' do
+ it 'refreshes the caches of the given types' do
+ expect(repository).to receive(:expire_method_caches).
+ with(%i(readme license_blob license_key))
- it { expect(Repository.remove_storage_from_path(full_path)).to eq(project_path) }
- it { expect(Repository.remove_storage_from_path(project_path)).to eq(project_path) }
- it { expect(Repository.remove_storage_from_path(storage_path)).to eq('') }
+ expect(repository).to receive(:readme)
+ expect(repository).to receive(:license_blob)
+ expect(repository).to receive(:license_key)
+
+ repository.refresh_method_caches(%i(readme license))
+ end
end
end
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
new file mode 100644
index 00000000000..6f491fdf9a0
--- /dev/null
+++ b/spec/models/route_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Route, models: true do
+ let!(:group) { create(:group) }
+ let!(:route) { group.route }
+
+ describe 'relationships' do
+ it { is_expected.to belong_to(:source) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:source) }
+ it { is_expected.to validate_presence_of(:path) }
+ it { is_expected.to validate_uniqueness_of(:path) }
+ end
+
+ describe '#rename_children' do
+ let!(:nested_group) { create(:group, path: "test", parent: group) }
+ let!(:deep_nested_group) { create(:group, path: "foo", parent: nested_group) }
+
+ it "updates children routes with new path" do
+ route.update_attributes(path: 'bar')
+
+ 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
+ end
+ end
+end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 43937a54b2c..691511cd93f 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -6,9 +6,6 @@ describe Service, models: true do
it { is_expected.to have_one :service_hook }
end
- describe "Mass assignment" do
- end
-
describe "Test Button" do
before do
@service = Service.new
@@ -53,7 +50,7 @@ describe Service, models: true do
describe "Template" do
describe "for pushover service" do
- let(:service_template) do
+ let!(:service_template) do
PushoverService.create(
template: true,
properties: {
@@ -66,13 +63,9 @@ describe Service, models: true do
let(:project) { create(:project) }
describe 'is prefilled for projects pushover service' do
- before do
- service_template
- project.build_missing_services
- end
-
it "has all fields prefilled" do
- service = project.pushover_service
+ service = project.find_or_initialize_service('pushover')
+
expect(service.template).to eq(false)
expect(service.device).to eq('MyDevice')
expect(service.sound).to eq('mic')
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index f62f6bacbaa..7425a897769 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -23,9 +23,9 @@ describe Snippet, models: true do
it { is_expected.to validate_presence_of(:author) }
it { is_expected.to validate_presence_of(:title) }
- it { is_expected.to validate_length_of(:title).is_within(0..255) }
+ it { is_expected.to validate_length_of(:title).is_at_most(255) }
- it { is_expected.to validate_length_of(:file_name).is_within(0..255) }
+ it { is_expected.to validate_length_of(:file_name).is_at_most(255) }
it { is_expected.to validate_presence_of(:content) }
@@ -33,16 +33,51 @@ describe Snippet, models: true do
end
describe '#to_reference' do
+ context 'when snippet belongs to a project' do
+ let(:project) { build(:empty_project, name: 'sample-project') }
+ let(:snippet) { build(:snippet, id: 1, project: project) }
+
+ it 'returns a String reference to the object' do
+ expect(snippet.to_reference).to eq "$1"
+ end
+
+ it 'supports a cross-project reference' do
+ another_project = build(:project, name: 'another-project', namespace: project.namespace)
+ expect(snippet.to_reference(another_project)).to eq "sample-project$1"
+ end
+ end
+
+ context 'when snippet does not belong to a project' do
+ let(:snippet) { build(:snippet, id: 1, project: nil) }
+
+ it 'returns a String reference to the object' do
+ expect(snippet.to_reference).to eq "$1"
+ end
+
+ it 'still returns shortest reference when project arg present' do
+ another_project = build(:project, name: 'another-project')
+ expect(snippet.to_reference(another_project)).to eq "$1"
+ end
+ end
+ end
+
+ describe '#file_name' do
let(:project) { create(:empty_project) }
- let(:snippet) { create(:snippet, project: project) }
- it 'returns a String reference to the object' do
- expect(snippet.to_reference).to eq "$#{snippet.id}"
+ context 'file_name is nil' do
+ let(:snippet) { create(:snippet, project: project, file_name: nil) }
+
+ it 'returns an empty string' do
+ expect(snippet.file_name).to eq ''
+ end
end
- it 'supports a cross-project reference' do
- cross = double('project')
- expect(snippet.to_reference(cross)).to eq "#{project.to_reference}$#{snippet.id}"
+ context 'file_name is not nil' do
+ let(:snippet) { create(:snippet, project: project, file_name: 'foo.txt') }
+
+ it 'returns the file_name' do
+ expect(snippet.file_name).to eq 'foo.txt'
+ end
end
end
diff --git a/spec/models/subscription_spec.rb b/spec/models/subscription_spec.rb
new file mode 100644
index 00000000000..9ab112bb2ee
--- /dev/null
+++ b/spec/models/subscription_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Subscription, models: true do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:subscribable) }
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:subscribable) }
+ it { is_expected.to validate_presence_of(:user) }
+
+ it 'validates uniqueness of project_id scoped to subscribable_id, subscribable_type, and user_id' do
+ create(:subscription)
+
+ expect(subject).to validate_uniqueness_of(:project_id).scoped_to([:subscribable_id, :subscribable_type, :user_id])
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 3b152e15b61..8b20ee81614 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -33,11 +33,12 @@ describe User, models: true do
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
it { is_expected.to have_many(:builds).dependent(:nullify) }
it { is_expected.to have_many(:pipelines).dependent(:nullify) }
+ it { is_expected.to have_many(:chat_names).dependent(:destroy) }
describe '#group_members' do
it 'does not include group memberships for which user is a requester' do
user = create(:user)
- group = create(:group, :public)
+ group = create(:group, :public, :access_requestable)
group.request_access(user)
expect(user.group_members).to be_empty
@@ -47,7 +48,7 @@ describe User, models: true do
describe '#project_members' do
it 'does not include project memberships for which user is a requester' do
user = create(:user)
- project = create(:project, :public)
+ project = create(:project, :public, :access_requestable)
project.request_access(user)
expect(user.project_members).to be_empty
@@ -78,7 +79,7 @@ describe User, models: true do
it { is_expected.to allow_value(0).for(:projects_limit) }
it { is_expected.not_to allow_value(-1).for(:projects_limit) }
- it { is_expected.to validate_length_of(:bio).is_within(0..255) }
+ it { is_expected.to validate_length_of(:bio).is_at_most(255) }
it_behaves_like 'an object with email-formated attributes', :email do
subject { build(:user) }
@@ -490,6 +491,28 @@ describe User, models: true do
end
end
+ describe '.without_projects' do
+ let!(:project) { create(:empty_project, :public, :access_requestable) }
+ let!(:user) { create(:user) }
+ let!(:user_without_project) { create(:user) }
+ let!(:user_without_project2) { create(:user) }
+
+ before do
+ # add user to project
+ project.team << [user, :master]
+
+ # create invite to projet
+ create(:project_member, :developer, project: project, invite_token: '1234', invite_email: 'inviteduser1@example.com')
+
+ # create request to join project
+ project.request_access(user_without_project2)
+ end
+
+ it { expect(User.without_projects).not_to include user }
+ it { expect(User.without_projects).to include user_without_project }
+ it { expect(User.without_projects).to include user_without_project2 }
+ end
+
describe '.not_in_project' do
before do
User.delete_all
@@ -552,6 +575,23 @@ describe User, models: true do
end
end
end
+
+ describe '#require_ssh_key?' do
+ protocol_and_expectation = {
+ 'http' => false,
+ 'ssh' => true,
+ '' => true,
+ }
+
+ protocol_and_expectation.each do |protocol, expected|
+ it "has correct require_ssh_key?" do
+ stub_application_setting(enabled_git_access_protocol: protocol)
+ user = build(:user)
+
+ expect(user.require_ssh_key?).to eq(expected)
+ end
+ end
+ end
end
describe '.find_by_any_email' do
@@ -687,17 +727,6 @@ describe User, models: true do
end
end
- describe 'by_username_or_id' do
- let(:user1) { create(:user, username: 'foo') }
-
- it "gets the correct user" do
- expect(User.by_username_or_id(user1.id)).to eq(user1)
- expect(User.by_username_or_id('foo')).to eq(user1)
- expect(User.by_username_or_id(-1)).to be_nil
- expect(User.by_username_or_id('bar')).to be_nil
- end
- end
-
describe '.find_by_ssh_key_id' do
context 'using an existing SSH key ID' do
let(:user) { create(:user) }
@@ -729,6 +758,17 @@ describe User, models: true do
end
end
+ describe '.find_by_username' do
+ it 'returns nil if not found' do
+ expect(described_class.find_by_username('JohnDoe')).to be_nil
+ end
+
+ it 'is case-insensitive' do
+ user = create(:user, username: 'JohnDoe')
+ expect(described_class.find_by_username('JOHNDOE')).to eq user
+ end
+ end
+
describe '.find_by_username!' do
it 'raises RecordNotFound' do
expect { described_class.find_by_username!('JohnDoe') }.
@@ -1050,7 +1090,7 @@ describe User, models: true do
it { is_expected.to eq([private_group]) }
end
- describe '#authorized_projects' do
+ describe '#authorized_projects', truncate: true do
context 'with a minimum access level' do
it 'includes projects for which the user is an owner' do
user = create(:user)
@@ -1070,6 +1110,80 @@ describe User, models: true do
.to contain_exactly(project)
end
end
+
+ it "includes user's personal projects" do
+ user = create(:user)
+ project = create(:project, :private, namespace: user.namespace)
+
+ expect(user.authorized_projects).to include(project)
+ end
+
+ it "includes personal projects user has been given access to" do
+ user1 = create(:user)
+ user2 = create(:user)
+ project = create(:project, :private, namespace: user1.namespace)
+
+ project.team << [user2, Gitlab::Access::DEVELOPER]
+
+ expect(user2.authorized_projects).to include(project)
+ end
+
+ it "includes projects of groups user has been added to" do
+ group = create(:group)
+ project = create(:project, group: group)
+ user = create(:user)
+
+ group.add_developer(user)
+
+ expect(user.authorized_projects).to include(project)
+ end
+
+ it "does not include projects of groups user has been removed from" do
+ group = create(:group)
+ project = create(:project, group: group)
+ user = create(:user)
+
+ member = group.add_developer(user)
+ expect(user.authorized_projects).to include(project)
+
+ member.destroy
+ expect(user.authorized_projects).not_to include(project)
+ end
+
+ it "includes projects shared with user's group" do
+ user = create(:user)
+ project = create(:project, :private)
+ group = create(:group)
+
+ group.add_reporter(user)
+ project.project_group_links.create(group: group)
+
+ expect(user.authorized_projects).to include(project)
+ end
+
+ it "does not include destroyed projects user had access to" do
+ user1 = create(:user)
+ user2 = create(:user)
+ project = create(:project, :private, namespace: user1.namespace)
+
+ project.team << [user2, Gitlab::Access::DEVELOPER]
+ expect(user2.authorized_projects).to include(project)
+
+ project.destroy
+ expect(user2.authorized_projects).not_to include(project)
+ end
+
+ it "does not include projects of destroyed groups user had access to" do
+ group = create(:group)
+ project = create(:project, namespace: group)
+ user = create(:user)
+
+ group.add_developer(user)
+ expect(user.authorized_projects).to include(project)
+
+ group.destroy
+ expect(user.authorized_projects).not_to include(project)
+ end
end
describe '#projects_where_can_admin_issues' do
@@ -1241,4 +1355,31 @@ describe User, models: true do
expect(projects).to be_empty
end
end
+
+ describe '#refresh_authorized_projects', redis: true do
+ let(:project1) { create(:empty_project) }
+ let(:project2) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ before do
+ project1.team << [user, :reporter]
+ project2.team << [user, :guest]
+
+ user.project_authorizations.delete_all
+ user.refresh_authorized_projects
+ end
+
+ it 'refreshes the list of authorized projects' do
+ expect(user.project_authorizations.count).to eq(2)
+ end
+
+ it 'sets the authorized_projects_populated column' do
+ expect(user.authorized_projects_populated).to eq(true)
+ end
+
+ it 'stores the correct access levels' do
+ expect(user.project_authorizations.where(access_level: Gitlab::Access::GUEST).exists?).to eq(true)
+ expect(user.project_authorizations.where(access_level: Gitlab::Access::REPORTER).exists?).to eq(true)
+ end
+ end
end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
new file mode 100644
index 00000000000..a20ac303a53
--- /dev/null
+++ b/spec/policies/group_policy_spec.rb
@@ -0,0 +1,108 @@
+require 'spec_helper'
+
+describe GroupPolicy, models: true do
+ let(:guest) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:master) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:group) { create(:group) }
+
+ let(:master_permissions) do
+ [
+ :create_projects,
+ :admin_milestones,
+ :admin_label
+ ]
+ end
+
+ let(:owner_permissions) do
+ [
+ :admin_group,
+ :admin_namespace,
+ :admin_group_member,
+ :change_visibility_level
+ ]
+ end
+
+ before do
+ group.add_guest(guest)
+ group.add_reporter(reporter)
+ group.add_developer(developer)
+ group.add_master(master)
+ group.add_owner(owner)
+ end
+
+ subject { described_class.abilities(current_user, group).to_set }
+
+ context 'with no user' do
+ let(:current_user) { nil }
+
+ it do
+ is_expected.to include(:read_group)
+ is_expected.not_to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'guests' do
+ let(:current_user) { guest }
+
+ it do
+ is_expected.to include(:read_group)
+ is_expected.not_to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'reporter' do
+ let(:current_user) { reporter }
+
+ it do
+ is_expected.to include(:read_group)
+ is_expected.not_to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'developer' do
+ let(:current_user) { developer }
+
+ it do
+ is_expected.to include(:read_group)
+ is_expected.not_to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'master' do
+ let(:current_user) { master }
+
+ it do
+ is_expected.to include(:read_group)
+ is_expected.to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'owner' do
+ let(:current_user) { owner }
+
+ it do
+ is_expected.to include(:read_group)
+ is_expected.to include(*master_permissions)
+ is_expected.to include(*owner_permissions)
+ end
+ end
+
+ context 'admin' do
+ let(:current_user) { admin }
+
+ it do
+ is_expected.to include(:read_group)
+ is_expected.to include(*master_permissions)
+ is_expected.to include(*owner_permissions)
+ end
+ end
+end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 96249a7d8c3..eeab9827d99 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -23,7 +23,7 @@ describe ProjectPolicy, models: true 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
+ :read_merge_request, :download_wiki_code
]
end
@@ -56,7 +56,8 @@ describe ProjectPolicy, models: true do
let(:public_permissions) do
[
:download_code, :fork_project, :read_commit_status, :read_pipeline,
- :read_container_image, :build_download_code, :build_read_container_image
+ :read_container_image, :build_download_code, :build_read_container_image,
+ :download_wiki_code
]
end
@@ -87,6 +88,15 @@ describe ProjectPolicy, models: true do
expect(Ability.allowed?(user, :read_issue, project)).to be_falsy
end
+ it 'does not include the wiki permissions when the feature is disabled' do
+ project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
+ wiki_permissions = [:read_wiki, :create_wiki, :update_wiki, :admin_wiki, :download_wiki_code]
+
+ permissions = described_class.abilities(owner, project).to_set
+
+ expect(permissions).not_to include(*wiki_permissions)
+ end
+
context 'abilities for non-public projects' do
let(:project) { create(:empty_project, namespace: owner.namespace) }
@@ -101,14 +111,36 @@ describe ProjectPolicy, models: true do
context 'guests' do
let(:current_user) { guest }
+ let(:reporter_public_build_permissions) do
+ reporter_permissions - [:read_build, :read_pipeline]
+ end
+
it do
is_expected.to include(*guest_permissions)
- is_expected.not_to include(*reporter_permissions)
+ is_expected.not_to include(*reporter_public_build_permissions)
is_expected.not_to include(*team_member_reporter_permissions)
is_expected.not_to include(*developer_permissions)
is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
+
+ context 'public builds enabled' do
+ it do
+ is_expected.to include(*guest_permissions)
+ is_expected.to include(:read_build, :read_pipeline)
+ end
+ end
+
+ context 'public builds disabled' do
+ before do
+ project.update(public_builds: false)
+ end
+
+ it do
+ is_expected.to include(*guest_permissions)
+ is_expected.not_to include(:read_build, :read_pipeline)
+ end
+ end
end
context 'reporter' do
diff --git a/spec/rake_helper.rb b/spec/rake_helper.rb
index 9b5b4bf9fea..298a520f5ca 100644
--- a/spec/rake_helper.rb
+++ b/spec/rake_helper.rb
@@ -8,7 +8,7 @@ RSpec.configure do |config|
config.before(:all) do
$stdout = StringIO.new
- Rake.application.rake_require 'tasks/gitlab/task_helpers'
+ Rake.application.rake_require 'tasks/gitlab/helpers'
Rake::Task.define_task :environment
end
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index b467890a403..1a771b3c87a 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -9,19 +9,19 @@ describe API::AccessRequests, api: true do
let(:stranger) { create(:user) }
let(:project) do
- project = create(:project, :public, creator_id: master.id, namespace: master.namespace)
- project.team << [developer, :developer]
- project.team << [master, :master]
- project.request_access(access_requester)
- project
+ create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
+ project.team << [developer, :developer]
+ project.team << [master, :master]
+ project.request_access(access_requester)
+ end
end
let(:group) do
- group = create(:group, :public)
- group.add_developer(developer)
- group.add_owner(master)
- group.request_access(access_requester)
- group
+ create(:group, :public, :access_requestable) do |group|
+ group.add_developer(developer)
+ group.add_owner(master)
+ group.request_access(access_requester)
+ end
end
shared_examples 'GET /:sources/:id/access_requests' do |source_type|
@@ -89,7 +89,7 @@ describe API::AccessRequests, api: true do
context 'when authenticated as a stranger' do
context "when access request is disabled for the #{source_type}" do
before do
- source.update(request_access_enabled: false)
+ source.update_attributes(request_access_enabled: false)
end
it 'returns 403' do
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
deleted file mode 100644
index 01bb9e955e0..00000000000
--- a/spec/requests/api/api_helpers_spec.rb
+++ /dev/null
@@ -1,293 +0,0 @@
-require 'spec_helper'
-
-describe API::Helpers, api: true do
- include API::Helpers
- include ApiHelpers
- include SentryHelper
-
- let(:user) { create(:user) }
- let(:admin) { create(:admin) }
- let(:key) { create(:key, user: user) }
-
- let(:params) { {} }
- let(:env) { { 'REQUEST_METHOD' => 'GET' } }
- let(:request) { Rack::Request.new(env) }
-
- def set_env(token_usr, identifier)
- clear_env
- clear_param
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = token_usr.private_token
- env[API::Helpers::SUDO_HEADER] = identifier
- end
-
- def set_param(token_usr, identifier)
- clear_env
- clear_param
- params[API::Helpers::PRIVATE_TOKEN_PARAM] = token_usr.private_token
- params[API::Helpers::SUDO_PARAM] = identifier
- end
-
- def clear_env
- env.delete(API::Helpers::PRIVATE_TOKEN_HEADER)
- env.delete(API::Helpers::SUDO_HEADER)
- end
-
- def clear_param
- params.delete(API::Helpers::PRIVATE_TOKEN_PARAM)
- params.delete(API::Helpers::SUDO_PARAM)
- end
-
- def warden_authenticate_returns(value)
- warden = double("warden", authenticate: value)
- env['warden'] = warden
- end
-
- def doorkeeper_guard_returns(value)
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ value }
- end
-
- def error!(message, status)
- raise Exception
- end
-
- describe ".current_user" do
- subject { current_user }
-
- describe "Warden authentication" do
- before { doorkeeper_guard_returns false }
-
- context "with invalid credentials" do
- context "GET request" do
- before { env['REQUEST_METHOD'] = 'GET' }
- it { is_expected.to be_nil }
- end
- end
-
- context "with valid credentials" do
- before { warden_authenticate_returns user }
-
- context "GET request" do
- before { env['REQUEST_METHOD'] = 'GET' }
- it { is_expected.to eq(user) }
- end
-
- context "HEAD request" do
- before { env['REQUEST_METHOD'] = 'HEAD' }
- it { is_expected.to eq(user) }
- end
-
- context "PUT request" do
- before { env['REQUEST_METHOD'] = 'PUT' }
- it { is_expected.to be_nil }
- end
-
- context "POST request" do
- before { env['REQUEST_METHOD'] = 'POST' }
- it { is_expected.to be_nil }
- end
-
- context "DELETE request" do
- before { env['REQUEST_METHOD'] = 'DELETE' }
- it { is_expected.to be_nil }
- end
- end
- end
-
- describe "when authenticating using a user's private token" do
- it "returns nil for an invalid token" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
- expect(current_user).to be_nil
- end
-
- it "returns nil for a user without access" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
- allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
- expect(current_user).to be_nil
- end
-
- it "leaves user as is when sudo not specified" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
- expect(current_user).to eq(user)
- clear_env
- params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token
- expect(current_user).to eq(user)
- end
- end
-
- describe "when authenticating using a user's personal access tokens" do
- let(:personal_access_token) { create(:personal_access_token, user: user) }
-
- it "returns nil for an invalid token" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
- expect(current_user).to be_nil
- end
-
- it "returns nil for a user without access" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
- expect(current_user).to be_nil
- end
-
- it "leaves user as is when sudo not specified" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- expect(current_user).to eq(user)
- clear_env
- params[API::Helpers::PRIVATE_TOKEN_PARAM] = personal_access_token.token
- expect(current_user).to eq(user)
- end
-
- it 'does not allow revoked tokens' do
- personal_access_token.revoke!
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
- expect(current_user).to be_nil
- end
-
- it 'does not allow expired tokens' do
- personal_access_token.update_attributes!(expires_at: 1.day.ago)
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
- expect(current_user).to be_nil
- end
- end
-
- it "changes current user to sudo when admin" do
- set_env(admin, user.id)
- expect(current_user).to eq(user)
- set_param(admin, user.id)
- expect(current_user).to eq(user)
- set_env(admin, user.username)
- expect(current_user).to eq(user)
- set_param(admin, user.username)
- expect(current_user).to eq(user)
- end
-
- it "throws an error when the current user is not an admin and attempting to sudo" do
- set_env(user, admin.id)
- expect { current_user }.to raise_error(Exception)
- set_param(user, admin.id)
- expect { current_user }.to raise_error(Exception)
- set_env(user, admin.username)
- expect { current_user }.to raise_error(Exception)
- set_param(user, admin.username)
- expect { current_user }.to raise_error(Exception)
- end
-
- it "throws an error when the user cannot be found for a given id" do
- id = user.id + admin.id
- expect(user.id).not_to eq(id)
- expect(admin.id).not_to eq(id)
- set_env(admin, id)
- expect { current_user }.to raise_error(Exception)
-
- set_param(admin, id)
- expect { current_user }.to raise_error(Exception)
- end
-
- it "throws an error when the user cannot be found for a given username" do
- username = "#{user.username}#{admin.username}"
- expect(user.username).not_to eq(username)
- expect(admin.username).not_to eq(username)
- set_env(admin, username)
- expect { current_user }.to raise_error(Exception)
-
- set_param(admin, username)
- expect { current_user }.to raise_error(Exception)
- end
-
- it "handles sudo's to oneself" do
- set_env(admin, admin.id)
- expect(current_user).to eq(admin)
- set_param(admin, admin.id)
- expect(current_user).to eq(admin)
- set_env(admin, admin.username)
- expect(current_user).to eq(admin)
- set_param(admin, admin.username)
- expect(current_user).to eq(admin)
- end
-
- it "handles multiple sudo's to oneself" do
- set_env(admin, user.id)
- expect(current_user).to eq(user)
- expect(current_user).to eq(user)
- set_env(admin, user.username)
- expect(current_user).to eq(user)
- expect(current_user).to eq(user)
-
- set_param(admin, user.id)
- expect(current_user).to eq(user)
- expect(current_user).to eq(user)
- set_param(admin, user.username)
- expect(current_user).to eq(user)
- expect(current_user).to eq(user)
- end
-
- it "handles multiple sudo's to oneself using string ids" do
- set_env(admin, user.id.to_s)
- expect(current_user).to eq(user)
- expect(current_user).to eq(user)
-
- set_param(admin, user.id.to_s)
- expect(current_user).to eq(user)
- expect(current_user).to eq(user)
- end
- end
-
- describe '.sudo_identifier' do
- it "returns integers when input is an int" do
- set_env(admin, '123')
- expect(sudo_identifier).to eq(123)
- set_env(admin, '0001234567890')
- expect(sudo_identifier).to eq(1234567890)
-
- set_param(admin, '123')
- expect(sudo_identifier).to eq(123)
- set_param(admin, '0001234567890')
- expect(sudo_identifier).to eq(1234567890)
- end
-
- it "returns string when input is an is not an int" do
- set_env(admin, '12.30')
- expect(sudo_identifier).to eq("12.30")
- set_env(admin, 'hello')
- expect(sudo_identifier).to eq('hello')
- set_env(admin, ' 123')
- expect(sudo_identifier).to eq(' 123')
-
- set_param(admin, '12.30')
- expect(sudo_identifier).to eq("12.30")
- set_param(admin, 'hello')
- expect(sudo_identifier).to eq('hello')
- set_param(admin, ' 123')
- expect(sudo_identifier).to eq(' 123')
- end
- end
-
- describe '.handle_api_exception' do
- before do
- allow_any_instance_of(self.class).to receive(:sentry_enabled?).and_return(true)
- allow_any_instance_of(self.class).to receive(:rack_response)
- end
-
- it 'does not report a MethodNotAllowed exception to Sentry' do
- exception = Grape::Exceptions::MethodNotAllowed.new({ 'X-GitLab-Test' => '1' })
- allow(exception).to receive(:backtrace).and_return(caller)
-
- expect(Raven).not_to receive(:capture_exception).with(exception)
-
- handle_api_exception(exception)
- end
-
- it 'does report RuntimeError to Sentry' do
- exception = RuntimeError.new('test error')
- allow(exception).to receive(:backtrace).and_return(caller)
-
- expect_any_instance_of(self.class).to receive(:sentry_context)
- expect(Raven).to receive(:capture_exception).with(exception)
-
- handle_api_exception(exception)
- end
- end
-end
diff --git a/spec/requests/api/api_internal_helpers_spec.rb b/spec/requests/api/api_internal_helpers_spec.rb
new file mode 100644
index 00000000000..be4bc39ada2
--- /dev/null
+++ b/spec/requests/api/api_internal_helpers_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe ::API::Helpers::InternalHelpers do
+ include ::API::Helpers::InternalHelpers
+
+ describe '.clean_project_path' do
+ project = 'namespace/project'
+ namespaced = File.join('namespace2', project)
+
+ {
+ File.join(Dir.pwd, project) => project,
+ File.join(Dir.pwd, namespaced) => namespaced,
+ project => project,
+ namespaced => namespaced,
+ project + '.git' => project,
+ namespaced + '.git' => namespaced,
+ "/" + project => project,
+ "/" + namespaced => namespaced,
+ }.each do |project_path, expected|
+ context project_path do
+ # Relative and absolute storage paths, with and without trailing /
+ ['.', './', Dir.pwd, Dir.pwd + '/'].each do |storage_path|
+ context "storage path is #{storage_path}" do
+ subject { clean_project_path(project_path, [storage_path]) }
+
+ it { is_expected.to eq(expected) }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 5ad4fc4865a..c8e8f31cc1f 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::AwardEmoji, api: true do
include ApiHelpers
let(:user) { create(:user) }
let!(:project) { create(:empty_project) }
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index 4f5c09a3029..3019724f52e 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::Boards, api: true do
include ApiHelpers
let(:user) { create(:user) }
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 1711096f4bd..2878e0cb59b 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require 'mime/types'
-describe API::API, api: true do
+describe API::Branches, api: true do
include ApiHelpers
let(:user) { create(:user) }
@@ -11,10 +11,11 @@ describe API::API, api: true do
let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:branch_name) { 'feature' }
let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
+ let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") }
describe "GET /projects/:id/repository/branches" do
it "returns an array of project branches" do
- project.repository.expire_cache
+ project.repository.expire_all_method_caches
get api("/projects/#{project.id}/repository/branches", user)
expect(response).to have_http_status(200)
@@ -31,11 +32,29 @@ describe API::API, api: true do
expect(json_response['name']).to eq(branch_name)
expect(json_response['commit']['id']).to eq(branch_sha)
+ expect(json_response['merged']).to eq(false)
expect(json_response['protected']).to eq(false)
expect(json_response['developers_can_push']).to eq(false)
expect(json_response['developers_can_merge']).to eq(false)
end
+ it "returns the branch information for a single branch with dots in the name" do
+ get api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq("with.1.2.3")
+ end
+
+ context 'on a merged branch' do
+ it "returns the branch information for a single branch" do
+ get api("/projects/#{project.id}/repository/branches/merge-test", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq('merge-test')
+ expect(json_response['merged']).to eq(true)
+ end
+ end
+
it "returns a 403 error if guest" do
get api("/projects/#{project.id}/repository/branches", user2)
expect(response).to have_http_status(403)
@@ -60,6 +79,14 @@ describe API::API, api: true do
expect(json_response['developers_can_merge']).to eq(false)
end
+ it "protects a single branch with dots in the name" do
+ put api("/projects/#{project.id}/repository/branches/with.1.2.3/protect", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq("with.1.2.3")
+ expect(json_response['protected']).to eq(true)
+ end
+
it 'protects a single branch and developers can push' do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
developers_can_push: true
@@ -209,6 +236,14 @@ describe API::API, api: true do
expect(json_response['protected']).to eq(false)
end
+ it "update branches with dots in branch name" do
+ put api("/projects/#{project.id}/repository/branches/with.1.2.3/unprotect", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq("with.1.2.3")
+ expect(json_response['protected']).to eq(false)
+ end
+
it "returns success when unprotect branch" do
put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user)
expect(response).to have_http_status(404)
@@ -281,6 +316,13 @@ describe API::API, api: true do
expect(json_response['branch_name']).to eq(branch_name)
end
+ it "removes a branch with dots in the branch name" do
+ delete api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['branch_name']).to eq("with.1.2.3")
+ end
+
it 'returns 404 if branch not exists' do
delete api("/projects/#{project.id}/repository/branches/foobar", user)
expect(response).to have_http_status(404)
@@ -299,4 +341,20 @@ describe API::API, api: true do
expect(json_response['message']).to eq('Cannot remove HEAD branch')
end
end
+
+ describe "DELETE /projects/:id/repository/merged_branches" do
+ before do
+ allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)
+ end
+
+ it 'returns 200' do
+ delete api("/projects/#{project.id}/repository/merged_branches", user)
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns a 403 error if guest' do
+ delete api("/projects/#{project.id}/repository/merged_branches", user2)
+ expect(response).to have_http_status(403)
+ end
+ end
end
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index fc72a44d663..7be7acebb19 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::Builds, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:api_user) { user }
- let!(:project) { create(:project, creator_id: user.id) }
+ let!(:project) { create(:project, creator_id: user.id, public_builds: false) }
let!(:developer) { create(:project_member, :developer, user: user, project: project) }
let(:reporter) { create(:project_member, :reporter, project: project) }
let(:guest) { create(:project_member, :guest, project: project) }
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index a6e8550fac3..964cded917c 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require 'mime/types'
-describe API::API, api: true do
+describe API::Commits, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
@@ -18,11 +18,14 @@ describe API::API, api: true do
before { project.team << [user2, :reporter] }
it "returns project commits" do
+ commit = project.repository.commit
get api("/projects/#{project.id}/repository/commits", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
- expect(json_response.first['id']).to eq(project.repository.commit.id)
+ expect(json_response.first['id']).to eq(commit.id)
+ expect(json_response.first['committer_name']).to eq(commit.committer_name)
+ expect(json_response.first['committer_email']).to eq(commit.committer_email)
end
end
@@ -134,6 +137,8 @@ describe API::API, api: true do
expect(response).to have_http_status(201)
expect(json_response['title']).to eq(message)
+ expect(json_response['committer_name']).to eq(user.name)
+ expect(json_response['committer_email']).to eq(user.email)
end
it 'returns a 400 bad request if file exists' do
@@ -451,6 +456,76 @@ describe API::API, api: true do
end
end
+ describe 'POST :id/repository/commits/:sha/cherry_pick' do
+ let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
+
+ context 'authorized user' do
+ it 'cherry picks a commit' do
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'master'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(master_pickable_commit.title)
+ expect(json_response['message']).to eq(master_pickable_commit.message)
+ expect(json_response['author_name']).to eq(master_pickable_commit.author_name)
+ expect(json_response['committer_name']).to eq(user.name)
+ end
+
+ it 'returns 400 if commit is already included in the target branch' do
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'markdown'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('Sorry, we cannot cherry-pick this commit automatically.
+ A cherry-pick may have already been performed with this commit, or a more recent commit may have updated some of its content.')
+ end
+
+ it 'returns 400 if you are not allowed to push to the target branch' do
+ project.team << [user2, :developer]
+ protected_branch = create(:protected_branch, project: project, name: 'feature')
+
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user2), branch: protected_branch.name
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('You are not allowed to push into this branch')
+ end
+
+ it 'returns 400 for missing parameters' do
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('branch is missing')
+ end
+
+ it 'returns 404 if commit is not found' do
+ post api("/projects/#{project.id}/repository/commits/abcd0123/cherry_pick", user), branch: 'master'
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Commit Not Found')
+ end
+
+ it 'returns 404 if branch is not found' do
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'foo'
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Branch Not Found')
+ end
+
+ it 'returns 400 for missing parameters' do
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('branch is missing')
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not cherry pick the commit' do
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick"), branch: 'master'
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
describe 'Post comment to commit' do
context 'authorized user' do
it 'returns comment' do
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index 65897edba7f..aabab8e6ae6 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::DeployKeys, api: true do
include ApiHelpers
let(:user) { create(:user) }
@@ -75,7 +75,6 @@ describe API::API, api: true do
expect(response).to have_http_status(400)
expect(json_response['message']['key']).to eq([
'can\'t be blank',
- 'is too short (minimum is 0 characters)',
'is invalid'
])
end
@@ -85,8 +84,7 @@ describe API::API, api: true do
expect(response).to have_http_status(400)
expect(json_response['message']['title']).to eq([
- 'can\'t be blank',
- 'is too short (minimum is 0 characters)'
+ 'can\'t be blank'
])
end
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index 8fa8c66db6c..31e3cfa1b2f 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::Deployments, api: true do
include ApiHelpers
let(:user) { create(:user) }
diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb
index 5262a623761..bd9ecaf2685 100644
--- a/spec/requests/api/doorkeeper_access_spec.rb
+++ b/spec/requests/api/doorkeeper_access_spec.rb
@@ -5,7 +5,7 @@ describe API::API, api: true do
let!(:user) { create(:user) }
let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) }
- let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id }
+ let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" }
describe "when unauthenticated" do
it "returns authentication success" do
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 1898b07835d..b9d535bc314 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::Environments, api: true do
include ApiHelpers
let(:user) { create(:user) }
@@ -46,6 +46,7 @@ describe API::API, api: true do
expect(response).to have_http_status(201)
expect(json_response['name']).to eq('mepmep')
+ expect(json_response['slug']).to eq('mepmep')
expect(json_response['external']).to be nil
end
@@ -60,6 +61,13 @@ describe API::API, api: true do
expect(response).to have_http_status(400)
end
+
+ it 'returns a 400 if slug is specified' do
+ post api("/projects/#{project.id}/environments", user), name: "foo", slug: "foo"
+
+ expect(response).to have_http_status(400)
+ expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed")
+ end
end
context 'a non member' do
@@ -86,6 +94,15 @@ describe API::API, api: true do
expect(json_response['external_url']).to eq(url)
end
+ it "won't allow slug to be changed" do
+ slug = environment.slug
+ api_url = api("/projects/#{project.id}/environments/#{environment.id}", user)
+ put api_url, slug: slug + "-foo"
+
+ expect(response).to have_http_status(400)
+ expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed")
+ end
+
it "won't update the external_url if only the name is passed" do
url = environment.external_url
put api("/projects/#{project.id}/environments/#{environment.id}", user),
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 050d0dd082d..2081f80ccc1 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::Files, api: true do
include ApiHelpers
let(:user) { create(:user) }
let!(:project) { create(:project, namespace: user.namespace ) }
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 7b47bf5afc1..cdeb965b413 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -1,14 +1,14 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::Groups, api: true do
include ApiHelpers
+ include UploadHelpers
let(:user1) { create(:user, can_create_group: false) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:admin) { create(:admin) }
- let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
- let!(:group1) { create(:group, avatar: File.open(avatar_file_path)) }
+ let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) }
let!(:group2) { create(:group, :private) }
let!(:project1) { create(:project, namespace: group1) }
let!(:project2) { create(:project, namespace: group2) }
@@ -57,13 +57,66 @@ describe API::API, api: true do
end
context "when using all_available in request" do
+ let(:response_groups) { json_response.map { |group| group['name'] } }
+
it "returns all groups you have access to" do
public_group = create :group, :public
get api("/groups", user1), all_available: true
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
- expect(json_response.first['name']).to eq(public_group.name)
+ expect(response_groups).to contain_exactly(public_group.name, group1.name)
+ end
+ end
+
+ context "when using sorting" do
+ let(:group3) { create(:group, name: "a#{group1.name}", path: "z#{group1.path}") }
+ let(:response_groups) { json_response.map { |group| group['name'] } }
+
+ before do
+ group3.add_owner(user1)
+ end
+
+ it "sorts by name ascending by default" do
+ get api("/groups", user1)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_groups).to eq([group3.name, group1.name])
+ end
+
+ it "sorts in descending order when passed" do
+ get api("/groups", user1), sort: "desc"
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_groups).to eq([group1.name, group3.name])
+ end
+
+ it "sorts by the order_by param" do
+ get api("/groups", user1), order_by: "path"
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_groups).to eq([group1.name, group3.name])
+ end
+ end
+ end
+
+ describe 'GET /groups/owned' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get api('/groups/owned')
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated as group owner' do
+ it 'returns an array of groups the user owns' do
+ get api('/groups/owned', user2)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(group2.name)
end
end
end
@@ -149,7 +202,7 @@ describe API::API, api: true do
end
it 'returns 404 for a non existing group' do
- put api('/groups/1328', user1)
+ put api('/groups/1328', user1), name: new_group_name
expect(response).to have_http_status(404)
end
@@ -190,6 +243,28 @@ describe API::API, api: true do
expect(json_response.length).to eq(2)
project_names = json_response.map { |proj| proj['name' ] }
expect(project_names).to match_array([project1.name, project3.name])
+ expect(json_response.first['default_branch']).to be_present
+ end
+
+ it "returns the group's projects with simple representation" do
+ get api("/groups/#{group1.id}/projects", user1), simple: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response.length).to eq(2)
+ project_names = json_response.map { |proj| proj['name' ] }
+ expect(project_names).to match_array([project1.name, project3.name])
+ expect(json_response.first['default_branch']).not_to be_present
+ end
+
+ it 'filters the groups projects' do
+ public_project = create(:project, :public, path: 'test1', group: group1)
+
+ get api("/groups/#{group1.id}/projects", user1), visibility: 'public'
+
+ 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(public_project.name)
end
it "does not return a non existing group" do
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
new file mode 100644
index 00000000000..c3d7ac3eef8
--- /dev/null
+++ b/spec/requests/api/helpers_spec.rb
@@ -0,0 +1,446 @@
+require 'spec_helper'
+
+describe API::Helpers, api: true do
+ include API::APIGuard::HelperMethods
+ include API::Helpers
+ include SentryHelper
+
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:key) { create(:key, user: user) }
+
+ let(:params) { {} }
+ let(:env) { { 'REQUEST_METHOD' => 'GET' } }
+ let(:request) { Rack::Request.new(env) }
+
+ def set_env(user_or_token, identifier)
+ clear_env
+ clear_param
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
+ env[API::Helpers::SUDO_HEADER] = identifier.to_s
+ end
+
+ def set_param(user_or_token, identifier)
+ clear_env
+ clear_param
+ params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
+ params[API::Helpers::SUDO_PARAM] = identifier.to_s
+ end
+
+ def clear_env
+ env.delete(API::APIGuard::PRIVATE_TOKEN_HEADER)
+ env.delete(API::Helpers::SUDO_HEADER)
+ end
+
+ def clear_param
+ params.delete(API::APIGuard::PRIVATE_TOKEN_PARAM)
+ params.delete(API::Helpers::SUDO_PARAM)
+ end
+
+ def warden_authenticate_returns(value)
+ warden = double("warden", authenticate: value)
+ env['warden'] = warden
+ end
+
+ def doorkeeper_guard_returns(value)
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ value }
+ end
+
+ def error!(message, status)
+ raise Exception.new("#{status} - #{message}")
+ end
+
+ describe ".current_user" do
+ subject { current_user }
+
+ describe "Warden authentication" do
+ before { doorkeeper_guard_returns false }
+
+ context "with invalid credentials" do
+ context "GET request" do
+ before { env['REQUEST_METHOD'] = 'GET' }
+ it { is_expected.to be_nil }
+ end
+ end
+
+ context "with valid credentials" do
+ before { warden_authenticate_returns user }
+
+ context "GET request" do
+ before { env['REQUEST_METHOD'] = 'GET' }
+ it { is_expected.to eq(user) }
+ end
+
+ context "HEAD request" do
+ before { env['REQUEST_METHOD'] = 'HEAD' }
+ it { is_expected.to eq(user) }
+ end
+
+ context "PUT request" do
+ before { env['REQUEST_METHOD'] = 'PUT' }
+ it { is_expected.to be_nil }
+ end
+
+ context "POST request" do
+ before { env['REQUEST_METHOD'] = 'POST' }
+ it { is_expected.to be_nil }
+ end
+
+ context "DELETE request" do
+ before { env['REQUEST_METHOD'] = 'DELETE' }
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+
+ describe "when authenticating using a user's private token" do
+ it "returns nil for an invalid token" do
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+
+ expect(current_user).to be_nil
+ end
+
+ it "returns nil for a user without access" do
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
+ allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
+
+ expect(current_user).to be_nil
+ end
+
+ it "leaves user as is when sudo not specified" do
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
+
+ expect(current_user).to eq(user)
+
+ clear_env
+
+ params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user.private_token
+
+ expect(current_user).to eq(user)
+ end
+ end
+
+ describe "when authenticating using a user's personal access tokens" do
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ before do
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false }
+ end
+
+ it "returns nil for an invalid token" do
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
+
+ expect(current_user).to be_nil
+ end
+
+ it "returns nil for a user without access" do
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
+
+ expect(current_user).to be_nil
+ end
+
+ it "returns nil for a token without the appropriate scope" do
+ personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ allow_access_with_scope('write_user')
+
+ expect(current_user).to be_nil
+ end
+
+ it "leaves user as is when sudo not specified" do
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ expect(current_user).to eq(user)
+ clear_env
+ params[API::APIGuard::PRIVATE_TOKEN_PARAM] = personal_access_token.token
+
+ expect(current_user).to eq(user)
+ end
+
+ it 'does not allow revoked tokens' do
+ personal_access_token.revoke!
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+
+ expect(current_user).to be_nil
+ end
+
+ it 'does not allow expired tokens' do
+ personal_access_token.update_attributes!(expires_at: 1.day.ago)
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+
+ expect(current_user).to be_nil
+ end
+ end
+
+ context 'sudo usage' do
+ context 'with admin' do
+ context 'with header' do
+ context 'with id' do
+ it 'changes current_user to sudo' do
+ set_env(admin, user.id)
+
+ expect(current_user).to eq(user)
+ end
+
+ it 'memoize the current_user: sudo permissions are not run against the sudoed user' do
+ set_env(admin, user.id)
+
+ expect(current_user).to eq(user)
+ expect(current_user).to eq(user)
+ end
+
+ it 'handles sudo to oneself' do
+ set_env(admin, admin.id)
+
+ expect(current_user).to eq(admin)
+ end
+
+ it 'throws an error when user cannot be found' do
+ id = user.id + admin.id
+ expect(user.id).not_to eq(id)
+ expect(admin.id).not_to eq(id)
+
+ set_env(admin, id)
+
+ expect { current_user }.to raise_error(Exception)
+ end
+ end
+
+ context 'with username' do
+ it 'changes current_user to sudo' do
+ set_env(admin, user.username)
+
+ expect(current_user).to eq(user)
+ end
+
+ it 'handles sudo to oneself' do
+ set_env(admin, admin.username)
+
+ expect(current_user).to eq(admin)
+ end
+
+ it "throws an error when the user cannot be found for a given username" do
+ username = "#{user.username}#{admin.username}"
+ expect(user.username).not_to eq(username)
+ expect(admin.username).not_to eq(username)
+
+ set_env(admin, username)
+
+ expect { current_user }.to raise_error(Exception)
+ end
+ end
+ end
+
+ context 'with param' do
+ context 'with id' do
+ it 'changes current_user to sudo' do
+ set_param(admin, user.id)
+
+ expect(current_user).to eq(user)
+ end
+
+ it 'handles sudo to oneself' do
+ set_param(admin, admin.id)
+
+ expect(current_user).to eq(admin)
+ end
+
+ it 'handles sudo to oneself using string' do
+ set_env(admin, user.id.to_s)
+
+ expect(current_user).to eq(user)
+ end
+
+ it 'throws an error when user cannot be found' do
+ id = user.id + admin.id
+ expect(user.id).not_to eq(id)
+ expect(admin.id).not_to eq(id)
+
+ set_param(admin, id)
+
+ expect { current_user }.to raise_error(Exception)
+ end
+ end
+
+ context 'with username' do
+ it 'changes current_user to sudo' do
+ set_param(admin, user.username)
+
+ expect(current_user).to eq(user)
+ end
+
+ it 'handles sudo to oneself' do
+ set_param(admin, admin.username)
+
+ expect(current_user).to eq(admin)
+ end
+
+ it "throws an error when the user cannot be found for a given username" do
+ username = "#{user.username}#{admin.username}"
+ expect(user.username).not_to eq(username)
+ expect(admin.username).not_to eq(username)
+
+ set_param(admin, username)
+
+ expect { current_user }.to raise_error(Exception)
+ end
+ end
+ end
+ end
+
+ context 'with regular user' do
+ context 'with env' do
+ it 'changes current_user to sudo when admin and user id' do
+ set_env(user, admin.id)
+
+ expect { current_user }.to raise_error(Exception)
+ end
+
+ it 'changes current_user to sudo when admin and user username' do
+ set_env(user, admin.username)
+
+ expect { current_user }.to raise_error(Exception)
+ end
+ end
+
+ context 'with params' do
+ it 'changes current_user to sudo when admin and user id' do
+ set_param(user, admin.id)
+
+ expect { current_user }.to raise_error(Exception)
+ end
+
+ it 'changes current_user to sudo when admin and user username' do
+ set_param(user, admin.username)
+
+ expect { current_user }.to raise_error(Exception)
+ end
+ end
+ end
+ end
+ end
+
+ describe '.sudo?' do
+ context 'when no sudo env or param is passed' do
+ before do
+ doorkeeper_guard_returns(nil)
+ end
+
+ it 'returns false' do
+ expect(sudo?).to be_falsy
+ end
+ end
+
+ context 'when sudo env or param is passed', 'user is not an admin' do
+ before do
+ set_env(user, '123')
+ end
+
+ it 'returns an 403 Forbidden' do
+ expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Must be admin to use sudo"}'
+ end
+ end
+
+ context 'when sudo env or param is passed', 'user is admin' do
+ context 'personal access token is used' do
+ before do
+ personal_access_token = create(:personal_access_token, user: admin)
+ set_env(personal_access_token.token, user.id)
+ end
+
+ it 'returns an 403 Forbidden' do
+ expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Private token must be specified in order to use sudo"}'
+ end
+ end
+
+ context 'private access token is used' do
+ before do
+ set_env(admin.private_token, user.id)
+ end
+
+ it 'returns true' do
+ expect(sudo?).to be_truthy
+ end
+ end
+ end
+ end
+
+ describe '.handle_api_exception' do
+ before do
+ allow_any_instance_of(self.class).to receive(:sentry_enabled?).and_return(true)
+ allow_any_instance_of(self.class).to receive(:rack_response)
+ end
+
+ it 'does not report a MethodNotAllowed exception to Sentry' do
+ exception = Grape::Exceptions::MethodNotAllowed.new({ 'X-GitLab-Test' => '1' })
+ allow(exception).to receive(:backtrace).and_return(caller)
+
+ expect(Raven).not_to receive(:capture_exception).with(exception)
+
+ handle_api_exception(exception)
+ end
+
+ it 'does report RuntimeError to Sentry' do
+ exception = RuntimeError.new('test error')
+ allow(exception).to receive(:backtrace).and_return(caller)
+
+ expect_any_instance_of(self.class).to receive(:sentry_context)
+ expect(Raven).to receive(:capture_exception).with(exception)
+
+ handle_api_exception(exception)
+ end
+ end
+
+ describe '.authenticate_non_get!' do
+ %w[HEAD GET].each do |method_name|
+ context "method is #{method_name}" do
+ before do
+ expect_any_instance_of(self.class).to receive(:route).and_return(double(route_method: method_name))
+ end
+
+ it 'does not raise an error' do
+ expect_any_instance_of(self.class).not_to receive(:authenticate!)
+
+ expect { authenticate_non_get! }.not_to raise_error
+ end
+ end
+ end
+
+ %w[POST PUT PATCH DELETE].each do |method_name|
+ context "method is #{method_name}" do
+ before do
+ expect_any_instance_of(self.class).to receive(:route).and_return(double(route_method: method_name))
+ end
+
+ it 'calls authenticate!' do
+ expect_any_instance_of(self.class).to receive(:authenticate!)
+
+ authenticate_non_get!
+ end
+ end
+ end
+ end
+
+ describe '.authenticate!' do
+ context 'current_user is nil' do
+ before do
+ expect_any_instance_of(self.class).to receive(:current_user).and_return(nil)
+ end
+
+ it 'returns a 401 response' do
+ expect { authenticate! }.to raise_error '401 - {"message"=>"401 Unauthorized"}'
+ end
+ end
+
+ context 'current_user is present' do
+ before do
+ expect_any_instance_of(self.class).to receive(:current_user).and_return(true)
+ end
+
+ it 'does not raise an error' do
+ expect { authenticate! }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index f0f590b0331..35644bd8cc9 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::Internal, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:key) { create(:key, user: user) }
let(:project) { create(:project) }
- let(:secret_token) { File.read Gitlab.config.gitlab_shell.secret_file }
+ let(:secret_token) { Gitlab::Shell.secret_token }
describe "GET /internal/check", no_db: true do
it do
@@ -191,6 +191,26 @@ describe API::API, api: true do
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
end
+
+ context 'project as /namespace/project' do
+ it do
+ pull(key, project_with_repo_path('/' + project.path_with_namespace))
+
+ expect(response).to have_http_status(200)
+ expect(json_response["status"]).to be_truthy
+ expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
+ end
+ end
+
+ context 'project as namespace/project' do
+ it do
+ pull(key, project_with_repo_path(project.path_with_namespace))
+
+ expect(response).to have_http_status(200)
+ expect(json_response["status"]).to be_truthy
+ expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
+ end
+ end
end
end
@@ -299,7 +319,7 @@ describe API::API, api: true do
context 'project does not exist' do
it do
- pull(key, OpenStruct.new(path_with_namespace: 'gitlab/notexists'))
+ pull(key, project_with_repo_path('gitlab/notexist'))
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey
@@ -386,17 +406,23 @@ describe API::API, api: true do
it 'returns link to create new merge request' do
expect(json_response).to match [{
"branch_name" => "new_branch",
- "url" => "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
+ "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
"new_merge_request" => true
}]
end
end
+ def project_with_repo_path(path)
+ double().tap do |fake_project|
+ allow(fake_project).to receive_message_chain('repository.path_to_repo' => path)
+ end
+ end
+
def pull(key, project, protocol = 'ssh')
post(
api("/internal/allowed"),
key_id: key.id,
- project: project.path_with_namespace,
+ project: project.repository.path_to_repo,
action: 'git-upload-pack',
secret_token: secret_token,
protocol: protocol
@@ -408,7 +434,7 @@ describe API::API, api: true do
api("/internal/allowed"),
changes: 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master',
key_id: key.id,
- project: project.path_with_namespace,
+ project: project.repository.path_to_repo,
action: 'git-receive-pack',
secret_token: secret_token,
protocol: protocol
@@ -420,7 +446,7 @@ describe API::API, api: true do
api("/internal/allowed"),
ref: 'master',
key_id: key.id,
- project: project.path_with_namespace,
+ project: project.repository.path_to_repo,
action: 'git-upload-archive',
secret_token: secret_token,
protocol: 'ssh'
@@ -432,7 +458,7 @@ describe API::API, api: true do
api("/internal/lfs_authenticate"),
key_id: key_id,
secret_token: secret_token,
- project: project.path_with_namespace
+ project: project.repository.path_to_repo
)
end
end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index beed53d1e5c..5c80dd98dc7 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::Issues, api: true do
include ApiHelpers
+ include EmailHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
@@ -71,13 +72,6 @@ describe API::API, api: true do
expect(json_response.last).to have_key('web_url')
end
- it "adds pagination headers and keep query params" do
- get api("/issues?state=closed&per_page=3", user)
- expect(response.headers['Link']).to eq(
- '<http://www.example.com/api/v3/issues?page=1&per_page=3&private_token=%s&state=closed>; rel="first", <http://www.example.com/api/v3/issues?page=1&per_page=3&private_token=%s&state=closed>; rel="last"' % [user.private_token, user.private_token]
- )
- end
-
it 'returns an array of closed issues' do
get api('/issues?state=closed', user)
expect(response).to have_http_status(200)
@@ -365,6 +359,24 @@ describe API::API, api: true do
let(:base_url) { "/projects/#{project.id}" }
let(:title) { milestone.title }
+ it "returns 404 on private projects for other users" do
+ private_project = create(:empty_project, :private)
+ create(:issue, project: private_project)
+
+ get 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 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 api("#{base_url}/issues", non_member)
expect(response).to have_http_status(200)
@@ -630,14 +642,13 @@ describe API::API, api: true do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', confidential: 'foo'
- expect(response).to have_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['confidential']).to be_falsy
+ 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)
+ label.toggle_subscription(user2, project)
perform_enqueued_jobs do
post api("/projects/#{project.id}/issues", user),
@@ -673,6 +684,32 @@ describe API::API, api: true do
])
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 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')
@@ -697,6 +734,14 @@ describe API::API, api: true do
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 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
@@ -809,8 +854,8 @@ describe API::API, api: true do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
confidential: 'foo'
- expect(response).to have_http_status(200)
- expect(json_response['confidential']).to be_truthy
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('confidential is invalid')
end
end
end
@@ -828,7 +873,7 @@ describe API::API, api: true do
it "sends notifications for subscribers of newly added labels when issue is updated" do
label = create(:label, title: 'foo', color: '#FFAABB', project: project)
- label.toggle_subscription(user2)
+ label.toggle_subscription(user2, project)
perform_enqueued_jobs do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
@@ -839,8 +884,8 @@ describe API::API, api: true do
end
it 'removes all labels' do
- put api("/projects/#{project.id}/issues/#{issue.id}", user),
- labels: ''
+ put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: ''
+
expect(response).to have_http_status(200)
expect(json_response['labels']).to eq([])
end
@@ -892,8 +937,8 @@ describe API::API, api: true do
update_time = 2.weeks.ago
put 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(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
@@ -932,6 +977,14 @@ describe API::API, api: true do
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 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
@@ -980,6 +1033,7 @@ describe API::API, api: true do
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
@@ -989,6 +1043,7 @@ describe API::API, api: true do
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
diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb
index 893ed5c2b10..4c80987d680 100644
--- a/spec/requests/api/keys_spec.rb
+++ b/spec/requests/api/keys_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::Keys, api: true do
include ApiHelpers
let(:user) { create(:user) }
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 2ff90b6deac..b29ce1ea25e 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::Labels, api: true do
include ApiHelpers
let(:user) { create(:user) }
@@ -15,8 +15,12 @@ describe API::API, api: true do
describe 'GET /projects/:id/labels' do
it 'returns all available labels to the project' do
group = create(:group)
- group_label = create(:group_label, group: group)
+ group_label = create(:group_label, title: 'feature', group: group)
project.update(group: group)
+ create(:labeled_issue, project: project, labels: [group_label], author: user)
+ create(:labeled_issue, project: project, labels: [label1], author: user, state: :closed)
+ create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project )
+
expected_keys = [
'id', 'name', 'color', 'description',
'open_issues_count', 'closed_issues_count', 'open_merge_requests_count',
@@ -30,14 +34,37 @@ describe API::API, api: true do
expect(json_response.size).to eq(3)
expect(json_response.first.keys).to match_array expected_keys
expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, priority_label.name, label1.name])
- expect(json_response.last['name']).to eq(label1.name)
- expect(json_response.last['color']).to be_present
- expect(json_response.last['description']).to be_nil
- expect(json_response.last['open_issues_count']).to eq(0)
- expect(json_response.last['closed_issues_count']).to eq(0)
- expect(json_response.last['open_merge_requests_count']).to eq(0)
- expect(json_response.last['priority']).to be_nil
- expect(json_response.last['subscribed']).to be_falsey
+
+ label1_response = json_response.find { |l| l['name'] == label1.title }
+ group_label_response = json_response.find { |l| l['name'] == group_label.title }
+ priority_label_response = json_response.find { |l| l['name'] == priority_label.title }
+
+ expect(label1_response['open_issues_count']).to eq(0)
+ expect(label1_response['closed_issues_count']).to eq(1)
+ expect(label1_response['open_merge_requests_count']).to eq(0)
+ expect(label1_response['name']).to eq(label1.name)
+ expect(label1_response['color']).to be_present
+ expect(label1_response['description']).to be_nil
+ expect(label1_response['priority']).to be_nil
+ expect(label1_response['subscribed']).to be_falsey
+
+ expect(group_label_response['open_issues_count']).to eq(1)
+ expect(group_label_response['closed_issues_count']).to eq(0)
+ expect(group_label_response['open_merge_requests_count']).to eq(0)
+ expect(group_label_response['name']).to eq(group_label.name)
+ expect(group_label_response['color']).to be_present
+ expect(group_label_response['description']).to be_nil
+ expect(group_label_response['priority']).to be_nil
+ expect(group_label_response['subscribed']).to be_falsey
+
+ expect(priority_label_response['open_issues_count']).to eq(0)
+ expect(priority_label_response['closed_issues_count']).to eq(0)
+ expect(priority_label_response['open_merge_requests_count']).to eq(1)
+ expect(priority_label_response['name']).to eq(priority_label.name)
+ expect(priority_label_response['color']).to be_present
+ expect(priority_label_response['description']).to be_nil
+ expect(priority_label_response['priority']).to eq(3)
+ expect(priority_label_response['subscribed']).to be_falsey
end
end
@@ -312,7 +339,7 @@ describe API::API, api: true do
end
context "when user is already subscribed to label" do
- before { label1.subscribe(user) }
+ before { label1.subscribe(user, project) }
it "returns 304" do
post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
@@ -331,7 +358,7 @@ describe API::API, api: true do
end
describe "DELETE /projects/:id/labels/:label_id/subscription" do
- before { label1.subscribe(user) }
+ before { label1.subscribe(user, project) }
context "when label_id is a label title" do
it "unsubscribes from the label" do
@@ -354,7 +381,7 @@ describe API::API, api: true do
end
context "when user is already unsubscribed from label" do
- before { label1.unsubscribe(user) }
+ before { label1.unsubscribe(user, project) }
it "returns 304" do
delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 493c0a893d1..2c94c86ccfa 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -9,19 +9,19 @@ describe API::Members, api: true do
let(:stranger) { create(:user) }
let(:project) do
- project = create(:project, :public, creator_id: master.id, namespace: master.namespace)
- project.team << [developer, :developer]
- project.team << [master, :master]
- project.request_access(access_requester)
- project
+ create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
+ project.team << [developer, :developer]
+ project.team << [master, :master]
+ project.request_access(access_requester)
+ end
end
let!(:group) do
- group = create(:group, :public)
- group.add_developer(developer)
- group.add_owner(master)
- group.request_access(access_requester)
- group
+ create(:group, :public, :access_requestable) do |group|
+ group.add_developer(developer)
+ group.add_owner(master)
+ group.request_access(access_requester)
+ end
end
shared_examples 'GET /:sources/:id/members' do |source_type|
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
index 131c2d406ea..e1887138aab 100644
--- a/spec/requests/api/merge_request_diffs_spec.rb
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-describe API::API, 'MergeRequestDiffs', api: true do
+describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do
include ApiHelpers
let!(:user) { create(:user) }
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index bae4fa11ec2..f032d1b683d 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-describe API::API, api: true do
+describe API::MergeRequests, api: true do
include ApiHelpers
let(:base_time) { Time.now }
let(:user) { create(:user) }
@@ -169,6 +169,16 @@ describe API::API, api: true do
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)
@@ -224,11 +234,14 @@ describe API::API, api: true do
target_branch: 'master',
author: user,
labels: 'label, label2',
- milestone_id: milestone.id
+ 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
@@ -392,14 +405,6 @@ describe API::API, api: true do
end
end
- describe "PUT /projects/:id/merge_requests/:merge_request_id to close MR" do
- it "returns merge_request" do
- put 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
-
describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do
let(:pipeline) { create(:ci_pipeline_without_jobs) }
@@ -463,8 +468,8 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
end
- it "enables merge when build succeeds if the ci is active" do
- allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
+ 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 api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true
@@ -476,6 +481,15 @@ describe API::API, api: true do
end
describe "PUT /projects/:id/merge_requests/:merge_request_id" do
+ context "to close a MR" do
+ it "returns merge_request" do
+ put 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 api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title"
expect(response).to have_http_status(200)
@@ -494,23 +508,24 @@ describe API::API, api: true do
expect(json_response['milestone']['id']).to eq(milestone.id)
end
- it "returns 400 when source_branch is specified" do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user),
- source_branch: "master", target_branch: "master"
- expect(response).to have_http_status(400)
- end
-
it "returns merge_request with renamed target_branch" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki"
expect(response).to have_http_status(200)
expect(json_response['target_branch']).to eq('wiki')
end
+ it "returns merge_request that removes the source branch" do
+ put 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 api("/projects/#{project.id}/merge_requests/#{merge_request.id}",
- user),
- title: 'new issue',
- labels: 'label, label?, label&foo, ?, &'
+ put 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?'
@@ -518,6 +533,22 @@ describe API::API, api: true do
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
+
+ it 'does not update state when title is empty' do
+ put 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 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
@@ -539,7 +570,7 @@ describe API::API, api: true do
it "returns 404 if note is attached to non existent merge request" do
post api("/projects/#{project.id}/merge_requests/404/comments", user),
- note: 'My comment'
+ note: 'My comment'
expect(response).to have_http_status(404)
end
end
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index dd192bea432..8beef821d6c 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::Milestones, api: true do
include ApiHelpers
let(:user) { create(:user) }
let!(:project) { create(:empty_project, namespace: user.namespace ) }
@@ -61,6 +61,15 @@ describe API::API, api: true do
expect(json_response.first['id']).to eq closed_milestone.id
end
+ it 'returns a project milestone by iid array' do
+ get api("/projects/#{project.id}/milestones", user), iid: [milestone.iid, closed_milestone.iid]
+
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(2)
+ expect(json_response.first['title']).to eq milestone.title
+ expect(json_response.first['id']).to eq milestone.id
+ end
+
it 'returns 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones/#{milestone.id}")
@@ -83,13 +92,14 @@ describe API::API, api: true do
expect(json_response['description']).to be_nil
end
- it 'creates a new project milestone with description and due date' do
+ it 'creates a new project milestone with description and dates' do
post api("/projects/#{project.id}/milestones", user),
- title: 'new milestone', description: 'release', due_date: '2013-03-02'
+ title: 'new milestone', description: 'release', due_date: '2013-03-02', start_date: '2013-02-02'
expect(response).to have_http_status(201)
expect(json_response['description']).to eq('release')
expect(json_response['due_date']).to eq('2013-03-02')
+ expect(json_response['start_date']).to eq('2013-02-02')
end
it 'returns a 400 error if title is missing' do
@@ -123,6 +133,15 @@ describe API::API, api: true do
expect(json_response['title']).to eq('updated title')
end
+ it 'removes a due date if nil is passed' do
+ milestone.update!(due_date: "2016-08-05")
+
+ put api("/projects/#{project.id}/milestones/#{milestone.id}", user), due_date: nil
+
+ expect(response).to have_http_status(200)
+ expect(json_response['due_date']).to be_nil
+ end
+
it 'returns a 404 error if milestone id not found' do
put api("/projects/#{project.id}/milestones/1234", user),
title: 'updated title'
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index 5347cf4f7bc..c1edf384d5c 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::Namespaces, api: true do
include ApiHelpers
let(:admin) { create(:admin) }
let(:user) { create(:user) }
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 0124b7271b3..028f93c8561 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::Notes, api: true do
include ApiHelpers
let(:user) { create(:user) }
let!(:project) { create(:project, :public, namespace: user.namespace) }
@@ -25,7 +25,7 @@ describe API::API, api: true do
let!(:cross_reference_note) do
create :note,
noteable: ext_issue, project: ext_proj,
- note: "Mentioned in issue #{private_issue.to_reference(ext_proj)}",
+ note: "mentioned in issue #{private_issue.to_reference(ext_proj)}",
system: true
end
diff --git a/spec/requests/api/notification_settings_spec.rb b/spec/requests/api/notification_settings_spec.rb
index e6d8a5ee954..8691a81420f 100644
--- a/spec/requests/api/notification_settings_spec.rb
+++ b/spec/requests/api/notification_settings_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::NotificationSettings, api: true do
include ApiHelpers
let(:user) { create(:user) }
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index 7011bdc9ec0..9a01f7fa1c4 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::Pipelines, api: true do
include ApiHelpers
let(:user) { create(:user) }
@@ -41,6 +41,52 @@ describe API::API, api: true do
end
end
+ describe 'POST /projects/:id/pipeline ' do
+ context 'authorized user' do
+ context 'with gitlab-ci.yml' do
+ before { stub_ci_pipeline_to_return_yaml_file }
+
+ it 'creates and returns a new pipeline' do
+ expect do
+ post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch
+ end.to change { Ci::Pipeline.count }.by(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response).to be_a Hash
+ expect(json_response['sha']).to eq project.commit.id
+ end
+
+ it 'fails when using an invalid ref' do
+ post api("/projects/#{project.id}/pipeline", user), ref: 'invalid_ref'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['base'].first).to eq 'Reference not found'
+ expect(json_response).not_to be_an Array
+ end
+ end
+
+ context 'without gitlab-ci.yml' do
+ it 'fails to create pipeline' do
+ post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['base'].first).to eq 'Missing .gitlab-ci.yml file'
+ expect(json_response).not_to be_an Array
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not create pipeline' do
+ post api("/projects/#{project.id}/pipeline", non_member), ref: project.default_branch
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(json_response).not_to be_an Array
+ end
+ end
+ end
+
describe 'GET /projects/:id/pipelines/:pipeline_id' do
context 'authorized user' do
it 'returns project pipelines' do
@@ -57,6 +103,18 @@ describe API::API, api: true do
expect(json_response['message']).to eq '404 Not found'
expect(json_response['id']).to be nil
end
+
+ context 'with coverage' do
+ before do
+ create(:ci_build, coverage: 30, pipeline: pipeline)
+ end
+
+ it 'exposes the coverage' do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
+
+ expect(json_response["coverage"].to_i).to eq(30)
+ end
+ end
end
context 'unauthorized user' do
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index 5f39329a1b8..a42cedae614 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, 'ProjectHooks', api: true do
+describe API::ProjectHooks, 'ProjectHooks', api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:user3) { create(:user) }
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 01148f0a05e..01032c0929b 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -1,12 +1,14 @@
require 'rails_helper'
-describe API::API, api: true do
+describe API::ProjectSnippets, api: true do
include ApiHelpers
+ let(:project) { create(:empty_project, :public) }
+ 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
- admin = create(:admin)
snippet = create(:project_snippet, author: admin)
get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin)
@@ -17,9 +19,9 @@ describe API::API, api: true do
end
describe 'GET /projects/:project_id/snippets/' do
+ let(:user) { create(:user) }
+
it 'returns all snippets available to team member' do
- project = create(:project, :public)
- user = create(:user)
project.team << [user, :developer]
public_snippet = create(:project_snippet, :public, project: project)
internal_snippet = create(:project_snippet, :internal, project: project)
@@ -34,8 +36,6 @@ describe API::API, api: true do
end
it 'hides private snippets from regular user' do
- project = create(:project, :public)
- user = create(:user)
create(:project_snippet, :private, project: project)
get api("/projects/#{project.id}/snippets/", user)
@@ -45,16 +45,16 @@ describe API::API, api: true do
end
describe 'POST /projects/:project_id/snippets/' do
- it 'creates a new snippet' do
- admin = create(:admin)
- project = create(:project)
- params = {
+ let(:params) do
+ {
title: 'Test Title',
file_name: 'test.rb',
code: 'puts "hello world"',
visibility_level: Gitlab::VisibilityLevel::PUBLIC
}
+ end
+ it 'creates a new snippet' do
post api("/projects/#{project.id}/snippets/", admin), params
expect(response).to have_http_status(201)
@@ -64,12 +64,20 @@ describe API::API, api: true do
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 api("/projects/#{project.id}/snippets/", admin), params
+
+ expect(response).to have_http_status(400)
+ end
end
describe 'PUT /projects/:project_id/snippets/:id/' do
+ let(:snippet) { create(:project_snippet, author: admin) }
+
it 'updates snippet' do
- admin = create(:admin)
- snippet = create(:project_snippet, author: admin)
new_content = 'New content'
put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), code: new_content
@@ -78,9 +86,24 @@ describe API::API, api: true do
snippet.reload
expect(snippet.content).to eq(new_content)
end
+
+ it 'returns 404 for invalid snippet id' do
+ put 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 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)
@@ -89,18 +112,31 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
end
+
+ it 'returns 404 for invalid snippet id' do
+ delete 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
- it 'returns raw text' do
- admin = create(:admin)
- snippet = create(:project_snippet, author: admin)
+ let(:snippet) { create(:project_snippet, author: admin) }
+ it 'returns raw text' do
get 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 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/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 3c8f0ac531a..8304c408064 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
require 'spec_helper'
-describe API::API, api: true do
+describe API::Projects, api: true do
include ApiHelpers
include Gitlab::CurrentSettings
let(:user) { create(:user) }
@@ -167,7 +167,7 @@ describe API::API, api: true do
expect(json_response).to satisfy do |response|
response.one? do |entry|
entry.has_key?('permissions') &&
- entry['name'] == project.name &&
+ entry['name'] == project.name &&
entry['owner']['username'] == user.username
end
end
@@ -175,33 +175,68 @@ describe API::API, api: true do
end
end
+ describe 'GET /projects/owned' do
+ before do
+ project3
+ project4
+ end
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get api('/projects/owned')
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated as project owner' do
+ it 'returns an array of projects the user owns' do
+ get api('/projects/owned', user4)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(project4.name)
+ expect(json_response.first['owner']['username']).to eq(user4.username)
+ end
+ end
+ end
+
describe 'GET /projects/visible' do
- let(:public_project) { create(:project, :public) }
+ shared_examples_for 'visible projects response' do
+ it 'returns the visible projects' do
+ get 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(:project, :public) }
before do
- public_project
project
project2
project3
project4
end
- it 'returns the projects viewable by the user' do
- get api('/projects/visible', user)
-
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.map { |project| project['id'] }).
- to contain_exactly(public_project.id, project.id, project2.id, project3.id)
+ context 'when unauthenticated' do
+ it_behaves_like 'visible projects response' do
+ let(:current_user) { nil }
+ let(:projects) { [public_project] }
+ end
end
- it 'shows only public projects when the user only has access to those' do
- get api('/projects/visible', user2)
+ context 'when authenticated' do
+ it_behaves_like 'visible projects response' do
+ let(:current_user) { user }
+ let(:projects) { [public_project, project, project2, project3] }
+ end
+ end
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.map { |project| project['id'] }).
- to contain_exactly(public_project.id)
+ 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
@@ -336,6 +371,14 @@ describe API::API, api: true do
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
end
+ it 'sets a project as allowing merge if only_allow_merge_if_all_discussions_are_resolved is nil' do
+ project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: nil)
+
+ post api('/projects', user), project
+
+ expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
+ end
+
it 'sets a project as allowing merge only if all discussions are resolved' do
project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true })
@@ -383,16 +426,7 @@ describe API::API, api: true do
not_to change { Project.count }
expect(response).to have_http_status(400)
- expect(json_response['message']['name']).to eq([
- 'can\'t be blank',
- 'is too short (minimum is 0 characters)',
- Gitlab::Regex.project_name_regex_message
- ])
- expect(json_response['message']['path']).to eq([
- 'can\'t be blank',
- 'is too short (minimum is 0 characters)',
- Gitlab::Regex.send(:project_path_regex_message)
- ])
+ expect(json_response['error']).to eq('name is missing')
end
it 'assigns attributes to project' do
@@ -406,6 +440,7 @@ describe API::API, api: true do
post 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)
@@ -415,6 +450,8 @@ describe API::API, api: true do
it 'sets a project as public' do
project = attributes_for(:project, :public)
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
@@ -422,6 +459,8 @@ describe API::API, api: true do
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
@@ -429,6 +468,8 @@ describe API::API, api: true do
it 'sets a project as internal' do
project = attributes_for(:project, :internal)
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
@@ -436,6 +477,7 @@ describe API::API, api: true do
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
@@ -497,135 +539,150 @@ describe API::API, api: true do
end
describe 'GET /projects/:id' do
- before { project }
- before { project_member }
-
- it 'returns a project by id' do
- group = create(:group)
- link = create(:project_group_link, project: project, group: group)
+ context 'when unauthenticated' do
+ it 'returns the public projects' do
+ public_project = create(:project, :public)
- get api("/projects/#{project.id}", user)
+ get api("/projects/#{public_project.id}")
- 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 api("/projects/#{project.id}", user)
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq(project.name)
+ 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
- it 'returns a 404 error if not found' do
- get api('/projects/42', user)
- expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Project Not Found')
- end
+ context 'when authenticated' do
+ before do
+ project
+ project_member
+ end
- it 'returns a 404 error if user is not a member' do
- other_user = create(:user)
- get api("/projects/#{project.id}", other_user)
- expect(response).to have_http_status(404)
- end
+ it 'returns a project by id' do
+ group = create(:group)
+ link = create(:project_group_link, project: project, group: group)
- it 'handles users with dots' do
- dot_user = create(:user, username: 'dot.user')
- project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace)
+ get api("/projects/#{project.id}", user)
- get 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
+ 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 api("/projects/#{project.id}", user)
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(project.name)
+ end
- describe 'permissions' do
- context 'all projects' do
- before { project.team << [user, :master] }
+ it 'returns a 404 error if not found' do
+ get api('/projects/42', user)
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
- it 'contains permission information' do
- get api("/projects", user)
+ it 'returns a 404 error if user is not a member' do
+ other_user = create(:user)
+ get api("/projects/#{project.id}", other_user)
+ expect(response).to have_http_status(404)
+ end
- 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
+ it 'handles users with dots' do
+ dot_user = create(:user, username: 'dot.user')
+ project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace)
+
+ get 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
- context 'personal project' do
- it 'sets project access and returns 200' do
- project.team << [user, :master]
- get api("/projects/#{project.id}", user)
+ describe 'permissions' do
+ context 'all projects' do
+ before { project.team << [user, :master] }
- expect(response).to have_http_status(200)
- expect(json_response['permissions']['project_access']['access_level']).
+ it 'contains permission information' do
+ get 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['permissions']['group_access']).to be_nil
+ expect(json_response.first['permissions']['group_access']).to be_nil
+ end
end
- end
- context 'group project' do
- let(:project2) { create(:project, group: create(:group)) }
+ context 'personal project' do
+ it 'sets project access and returns 200' do
+ project.team << [user, :master]
+ get api("/projects/#{project.id}", user)
- before { project2.group.add_owner(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
- it 'sets the owner and return 200' do
- get api("/projects/#{project2.id}", user)
+ context 'group project' do
+ let(:project2) { create(:project, group: create(:group)) }
- expect(response).to have_http_status(200)
- expect(json_response['permissions']['project_access']).to be_nil
- expect(json_response['permissions']['group_access']['access_level']).
+ before { project2.group.add_owner(user) }
+
+ it 'sets the owner and return 200' do
+ get 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
- before { project_member2 }
-
- context 'valid request' do
- before 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)
- end
- it 'returns all events' do
- get api("/projects/#{project.id}/events", user)
+ get api("/projects/#{project.id}/events", current_user)
expect(response).to have_http_status(200)
@@ -638,24 +695,90 @@ describe API::API, api: true do
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(user3.username)
- expect(last_event['author']['name']).to eq(user3.name)
+ expect(last_event['author_username']).to eq(member.username)
+ expect(last_event['author']['name']).to eq(member.name)
end
end
- it 'returns a 404 error if not found' do
- get api('/projects/42/events', user)
+ context 'when unauthenticated' do
+ it_behaves_like 'project events response' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { nil }
+ end
+ end
- expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Project Not Found')
+ 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 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 api("/projects/#{project.id}/events", other_user)
+
+ expect(response).to have_http_status(404)
+ end
end
+ end
- it 'returns a 404 error if user is not a member' do
- other_user = create(:user)
+ 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 api("/projects/#{project.id}/events", other_user)
+ get api("/projects/#{project.id}/users", current_user)
- expect(response).to have_http_status(404)
+ 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(: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 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 api("/projects/#{project.id}/users", other_user)
+
+ expect(response).to have_http_status(404)
+ end
end
end
@@ -816,7 +939,7 @@ describe API::API, api: true do
it 'is idempotent if not forked' do
expect(project_fork_target.forked_from_project).to be_nil
delete api("/projects/#{project_fork_target.id}/fork", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(304)
expect(project_fork_target.reload.forked_from_project).to be_nil
end
end
@@ -833,7 +956,7 @@ describe API::API, api: true do
post 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.status).to eq 201
+ 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)
@@ -841,18 +964,18 @@ describe API::API, api: true do
it "returns a 400 error when group id is not given" do
post api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER
- expect(response.status).to eq 400
+ expect(response).to have_http_status(400)
end
it "returns a 400 error when access level is not given" do
post api("/projects/#{project.id}/share", user), group_id: group.id
- expect(response.status).to eq 400
+ 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 api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
- expect(response.status).to eq 400
+ expect(response).to have_http_status(400)
end
it 'returns a 404 error when user cannot read group' do
@@ -860,19 +983,50 @@ describe API::API, api: true do
post api("/projects/#{project.id}/share", user), group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER
- expect(response.status).to eq 404
+ expect(response).to have_http_status(404)
end
it 'returns a 404 error when group does not exist' do
post api("/projects/#{project.id}/share", user), group_id: 1234, group_access: Gitlab::Access::DEVELOPER
- expect(response.status).to eq 404
+ expect(response).to have_http_status(404)
end
- it "returns a 409 error when wrong params passed" do
+ it "returns a 400 error when wrong params passed" do
post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234
- expect(response.status).to eq 409
- expect(json_response['message']).to eq 'Group access is not included in the list'
+
+ 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 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 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 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 api("/projects/123/share/1234", user)
+
+ expect(response).to have_http_status(404)
end
end
@@ -888,35 +1042,37 @@ describe API::API, api: true do
let!(:public) { create(:empty_project, :public, name: "public #{query}") }
let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') }
+ shared_examples_for 'project search response' do |args = {}|
+ it 'returns project search responses' do
+ get api("/projects/search/#{query}", current_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(args[:results])
+ json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*query.*/) }
+ end
+ end
+
context 'when unauthenticated' do
- it 'returns authentication error' do
- get api("/projects/search/#{query}")
- expect(response).to have_http_status(401)
+ it_behaves_like 'project search response', results: 1 do
+ let(:current_user) { nil }
end
end
context 'when authenticated' do
- it 'returns an array of projects' do
- get api("/projects/search/#{query}", user)
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(6)
- json_response.each {|project| expect(project['name']).to match(/.*query.*/)}
+ it_behaves_like 'project search response', results: 6 do
+ let(:current_user) { user }
end
end
context 'when authenticated as a different user' do
- it 'returns matching public projects' do
- get api("/projects/search/#{query}", user2)
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(2)
- json_response.each {|project| expect(project['name']).to match(/(internal|public) query/)}
+ it_behaves_like 'project search response', results: 2, match_regex: /(internal|public) query/ do
+ let(:current_user) { user2 }
end
end
end
- describe 'PUT /projects/:id̈́' do
+ describe 'PUT /projects/:id' do
before { project }
before { user }
before { user3 }
@@ -955,7 +1111,6 @@ describe API::API, api: true do
it 'updates visibility_level from public to private' do
project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
-
project_param = { public: false }
put api("/projects/#{project3.id}", user), project_param
expect(response).to have_http_status(200)
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index c4dc2d9006a..c90b69e8ebb 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require 'mime/types'
-describe API::API, api: true do
+describe API::Repositories, api: true do
include ApiHelpers
include RepoHelpers
include WorkhorseHelpers
@@ -18,6 +18,7 @@ describe API::API, api: true do
it "returns project commits" do
get api("/projects/#{project.id}/repository/tree", user)
+
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -43,6 +44,39 @@ describe API::API, api: true do
end
end
+ describe 'GET /projects/:id/repository/tree?recursive=1' do
+ context 'authorized user' do
+ before { project.team << [user2, :reporter] }
+
+ it 'should return recursive project paths tree' do
+ get api("/projects/#{project.id}/repository/tree?recursive=1", user)
+
+ expect(response.status).to eq(200)
+
+ expect(json_response).to be_an Array
+ expect(json_response[4]['name']).to eq('html')
+ expect(json_response[4]['path']).to eq('files/html')
+ expect(json_response[4]['type']).to eq('tree')
+ expect(json_response[4]['mode']).to eq('040000')
+ end
+
+ it 'returns a 404 for unknown ref' do
+ get api("/projects/#{project.id}/repository/tree?ref_name=foo&recursive=1", user)
+ expect(response).to have_http_status(404)
+
+ expect(json_response).to be_an Object
+ json_response['message'] == '404 Tree Not Found'
+ end
+ end
+
+ context "unauthorized user" do
+ it "does not return project commits" do
+ get api("/projects/#{project.id}/repository/tree?recursive=1")
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
describe "GET /projects/:id/repository/blobs/:sha" do
it "gets the raw file contents" do
get api("/projects/#{project.id}/repository/blobs/master?filepath=README.md", user)
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index f46f016135e..99414270be6 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -226,7 +226,7 @@ describe API::Runners, api: true do
context 'authorized user' do
context 'when runner is shared' do
it 'does not update runner' do
- put api("/runners/#{shared_runner.id}", user)
+ put api("/runners/#{shared_runner.id}", user), description: 'test'
expect(response).to have_http_status(403)
end
@@ -234,7 +234,7 @@ describe API::Runners, api: true do
context 'when runner is not shared' do
it 'does not update runner without access to it' do
- put api("/runners/#{specific_runner.id}", user2)
+ put api("/runners/#{specific_runner.id}", user2), description: 'test'
expect(response).to have_http_status(403)
end
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index 375671bca4c..668e39f9dba 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -1,7 +1,8 @@
require "spec_helper"
-describe API::API, api: true do
+describe API::Services, api: true do
include ApiHelpers
+
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:user2) { create(:user) }
@@ -56,8 +57,7 @@ describe API::API, api: true do
# inject some properties into the service
before do
- project.build_missing_services
- service_object = project.send(service_method)
+ service_object = project.find_or_initialize_service(service)
service_object.properties = service_attrs
service_object.save
end
@@ -89,4 +89,61 @@ describe API::API, api: true do
end
end
end
+
+ describe 'POST /projects/:id/services/:slug/trigger' do
+ let!(:project) { create(:empty_project) }
+ let(:service_name) { 'mattermost_slash_commands' }
+
+ context 'no service is available' do
+ it 'returns a not found message' do
+ post api("/projects/#{project.id}/services/idonotexist/trigger")
+
+ expect(response).to have_http_status(404)
+ expect(json_response["error"]).to eq("404 Not Found")
+ end
+ end
+
+ context 'the service exists' do
+ let(:params) { { token: 'token' } }
+
+ context 'the service is not active' do
+ let!(:inactive_service) do
+ project.create_mattermost_slash_commands_service(
+ active: false,
+ properties: { token: 'token' }
+ )
+ end
+
+ it 'when the service is inactive' do
+ post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger"), params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'the service is active' do
+ let!(:active_service) do
+ project.create_mattermost_slash_commands_service(
+ active: true,
+ properties: { token: 'token' }
+ )
+ end
+
+ it 'returns status 200' do
+ post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger"), params
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ context 'when the project can not be found' do
+ it 'returns a generic 404' do
+ post api("/projects/404/services/mattermost_slash_commands/trigger"), params
+
+ expect(response).to have_http_status(404)
+ expect(json_response["message"]).to eq("404 Service Not Found")
+ end
+ end
+ end
+ end
end
diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb
index acad1365ace..794e2b5c04d 100644
--- a/spec/requests/api/session_spec.rb
+++ b/spec/requests/api/session_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::Session, api: true do
include ApiHelpers
let(:user) { create(:user) }
@@ -67,22 +67,24 @@ describe API::API, api: true do
end
context "when empty password" do
- it "returns authentication error" do
+ it "returns authentication error with email" do
post api("/session"), email: user.email
- expect(response).to have_http_status(401)
- expect(json_response['email']).to be_nil
- expect(json_response['private_token']).to be_nil
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns authentication error with username" do
+ post api("/session"), email: user.username
+
+ expect(response).to have_http_status(400)
end
end
context "when empty name" do
it "returns authentication error" do
post api("/session"), password: user.password
- expect(response).to have_http_status(401)
- expect(json_response['email']).to be_nil
- expect(json_response['private_token']).to be_nil
+ expect(response).to have_http_status(400)
end
end
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 096a8ebab70..9a8d633d657 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, 'Settings', api: true do
+describe API::Settings, 'Settings', api: true do
include ApiHelpers
let(:user) { create(:user) }
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
new file mode 100644
index 00000000000..f6fb6ea5506
--- /dev/null
+++ b/spec/requests/api/snippets_spec.rb
@@ -0,0 +1,157 @@
+require 'rails_helper'
+
+describe API::Snippets, api: true do
+ include ApiHelpers
+ let!(:user) { create(:user) }
+
+ describe 'GET /snippets/' do
+ it 'returns snippets available' do
+ public_snippet = create(:personal_snippet, :public, author: user)
+ private_snippet = create(:personal_snippet, :private, author: user)
+ internal_snippet = create(:personal_snippet, :internal, author: user)
+
+ get api("/snippets/", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
+ public_snippet.id,
+ internal_snippet.id,
+ private_snippet.id)
+ expect(json_response.last).to have_key('web_url')
+ expect(json_response.last).to have_key('raw_url')
+ end
+
+ it 'hides private snippets from regular user' do
+ create(:personal_snippet, :private)
+
+ get api("/snippets/", user)
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(0)
+ end
+ end
+
+ describe 'GET /snippets/public' do
+ let!(:other_user) { create(:user) }
+ let!(:public_snippet) { create(:personal_snippet, :public, author: user) }
+ let!(:private_snippet) { create(:personal_snippet, :private, author: user) }
+ let!(:internal_snippet) { create(:personal_snippet, :internal, author: user) }
+ let!(:public_snippet_other) { create(:personal_snippet, :public, author: other_user) }
+ let!(:private_snippet_other) { create(:personal_snippet, :private, author: other_user) }
+ let!(:internal_snippet_other) { create(:personal_snippet, :internal, author: other_user) }
+
+ it 'returns all snippets with public visibility from all users' do
+ get api("/snippets/public", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
+ public_snippet.id,
+ public_snippet_other.id)
+ expect(json_response.map{ |snippet| snippet['web_url']} ).to include(
+ "http://localhost/snippets/#{public_snippet.id}",
+ "http://localhost/snippets/#{public_snippet_other.id}")
+ expect(json_response.map{ |snippet| snippet['raw_url']} ).to include(
+ "http://localhost/snippets/#{public_snippet.id}/raw",
+ "http://localhost/snippets/#{public_snippet_other.id}/raw")
+ end
+ end
+
+ describe 'GET /snippets/:id/raw' do
+ let(:snippet) { create(:personal_snippet, author: user) }
+
+ it 'returns raw text' do
+ get api("/snippets/#{snippet.id}/raw", user)
+
+ 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 api("/snippets/1234", user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+ end
+
+ describe 'POST /snippets/' do
+ let(:params) do
+ {
+ title: 'Test Title',
+ file_name: 'test.rb',
+ content: 'puts "hello world"',
+ visibility_level: Gitlab::VisibilityLevel::PUBLIC
+ }
+ end
+
+ it 'creates a new snippet' do
+ expect do
+ post api("/snippets/", user), params
+ end.to change { PersonalSnippet.count }.by(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(params[:title])
+ expect(json_response['file_name']).to eq(params[:file_name])
+ end
+
+ it 'returns 400 for missing parameters' do
+ params.delete(:title)
+
+ post api("/snippets/", user), params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ describe 'PUT /snippets/:id' do
+ let(:other_user) { create(:user) }
+ let(:public_snippet) { create(:personal_snippet, :public, author: user) }
+ it 'updates snippet' do
+ new_content = 'New content'
+
+ put api("/snippets/#{public_snippet.id}", user), content: new_content
+
+ expect(response).to have_http_status(200)
+ public_snippet.reload
+ expect(public_snippet.content).to eq(new_content)
+ end
+
+ it 'returns 404 for invalid snippet id' do
+ put api("/snippets/1234", user), title: 'foo'
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+
+ it "returns 404 for another user's snippet" do
+ put api("/snippets/#{public_snippet.id}", other_user), title: 'fubar'
+
+ 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 api("/snippets/1234", user)
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ describe 'DELETE /snippets/:id' do
+ let!(:public_snippet) { create(:personal_snippet, :public, author: user) }
+ it 'deletes snippet' do
+ expect do
+ delete api("/snippets/#{public_snippet.id}", user)
+
+ expect(response).to have_http_status(204)
+ end.to change { PersonalSnippet.count }.by(-1)
+ end
+
+ it 'returns 404 for invalid snippet id' do
+ delete api("/snippets/1234", user)
+
+ 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/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index 6c9df21f598..b3e5afdadb1 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::SystemHooks, api: true do
include ApiHelpers
let(:user) { create(:user) }
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index d563883cd47..a1c32ae65ba 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require 'mime/types'
-describe API::API, api: true do
+describe API::Tags, api: true do
include ApiHelpers
include RepoHelpers
@@ -15,6 +15,31 @@ describe API::API, api: true do
let(:tag_name) { project.repository.tag_names.sort.reverse.first }
let(:description) { 'Awesome release!' }
+ shared_examples_for 'repository tags' do
+ it 'returns the repository tags' do
+ get api("/projects/#{project.id}/repository/tags", current_user)
+
+ expect(response).to have_http_status(200)
+
+ first_tag = json_response.first
+
+ expect(first_tag['name']).to eq(tag_name)
+ end
+ end
+
+ context 'when unauthenticated' do
+ it_behaves_like 'repository tags' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when authenticated' do
+ it_behaves_like 'repository tags' do
+ let(:current_user) { user }
+ end
+ end
+
context 'without releases' do
it "returns an array of project tags" do
get api("/projects/#{project.id}/repository/tags", user)
@@ -45,17 +70,33 @@ describe API::API, api: true do
describe 'GET /projects/:id/repository/tags/:tag_name' do
let(:tag_name) { project.repository.tag_names.sort.reverse.first }
- it 'returns a specific tag' do
- get api("/projects/#{project.id}/repository/tags/#{tag_name}", user)
+ shared_examples_for 'repository tag' do
+ it 'returns the repository tag' do
+ get api("/projects/#{project.id}/repository/tags/#{tag_name}", current_user)
+
+ expect(response).to have_http_status(200)
+
+ expect(json_response['name']).to eq(tag_name)
+ end
+
+ it 'returns 404 for an invalid tag name' do
+ get api("/projects/#{project.id}/repository/tags/foobar", current_user)
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq(tag_name)
+ expect(response).to have_http_status(404)
+ end
end
- it 'returns 404 for an invalid tag name' do
- get api("/projects/#{project.id}/repository/tags/foobar", user)
+ context 'when unauthenticated' do
+ it_behaves_like 'repository tag' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { nil }
+ end
+ end
- expect(response).to have_http_status(404)
+ context 'when authenticated' do
+ it_behaves_like 'repository tag' do
+ let(:current_user) { user }
+ end
end
end
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 82bba1ce8a4..67ec3168679 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API do
+describe API::Triggers do
include ApiHelpers
let(:user) { create(:user) }
@@ -54,6 +54,13 @@ describe API::API do
expect(pipeline.builds.size).to eq(5)
end
+ it 'creates builds on webhook from other gitlab repository and branch' do
+ expect do
+ post api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+ end.to change(project.builds, :count).by(5)
+ expect(response).to have_http_status(201)
+ end
+
it 'returns bad request with no builds created if there\'s no commit for that ref' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch')
expect(response).to have_http_status(400)
@@ -68,7 +75,7 @@ describe API::API do
it 'validates variables to be a hash' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master')
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('variables needs to be a hash')
+ expect(json_response['error']).to eq('variables is invalid')
end
it 'validates variables needs to be a map of key-valued strings' do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index ae8639d78d5..9e317f3a7e9 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::Users, api: true do
include ApiHelpers
let(:user) { create(:user) }
@@ -48,6 +48,17 @@ describe API::API, api: true do
end['username']).to eq(username)
end
+ it "returns an array of blocked users" do
+ ldap_blocked_user
+ create(:user, state: 'blocked')
+
+ get api("/users?blocked=true", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response).to all(include('state' => /(blocked|ldap_blocked)/))
+ end
+
it "returns one user" do
get api("/users?username=#{omniauth_user.username}", user)
expect(response).to have_http_status(200)
@@ -69,6 +80,16 @@ describe API::API, api: true do
expect(json_response.first.keys).to include 'last_sign_in_at'
expect(json_response.first.keys).to include 'confirmed_at'
end
+
+ it "returns an array of external users" do
+ create(:user, external: true)
+
+ get api("/users?external=true", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response).to all(include('external' => true))
+ end
end
end
@@ -87,7 +108,7 @@ describe API::API, api: true do
it "returns a 404 error if user id not found" do
get api("/users/9999", user)
expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Not found')
+ expect(json_response['message']).to eq('404 User Not Found')
end
it "returns a 404 for invalid ID" do
@@ -338,7 +359,7 @@ describe API::API, api: true do
it "returns 404 for non-existing user" do
put api("/users/999999", admin), { bio: 'update should fail' }
expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Not found')
+ expect(json_response['message']).to eq('404 User Not Found')
end
it "returns a 404 if invalid ID" do
@@ -366,6 +387,18 @@ describe API::API, api: true do
to eq([Gitlab::Regex.namespace_regex_message])
end
+ it 'returns 400 if provider is missing for identity update' do
+ put api("/users/#{omniauth_user.id}", admin), extern_uid: '654321'
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns 400 if external UID is missing for identity update' do
+ put api("/users/#{omniauth_user.id}", admin), provider: 'ldap'
+
+ expect(response).to have_http_status(400)
+ end
+
context "with existing user" do
before do
post api("/users", admin), { email: 'test@example.com', password: 'password', username: 'test', name: 'test' }
@@ -393,14 +426,16 @@ describe API::API, api: true do
it "does not create invalid ssh key" do
post api("/users/#{user.id}/keys", admin), { title: "invalid key" }
+
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('400 (Bad request) "key" not given')
+ expect(json_response['error']).to eq('key is missing')
end
it 'does not create key without title' do
post api("/users/#{user.id}/keys", admin), key: 'some key'
+
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('400 (Bad request) "title" not given')
+ expect(json_response['error']).to eq('title is missing')
end
it "creates ssh key" do
@@ -416,7 +451,7 @@ describe API::API, api: true do
end
end
- describe 'GET /user/:uid/keys' do
+ describe 'GET /user/:id/keys' do
before { admin }
context 'when unauthenticated' do
@@ -444,7 +479,7 @@ describe API::API, api: true do
end
end
- describe 'DELETE /user/:uid/keys/:id' do
+ describe 'DELETE /user/:id/keys/:key_id' do
before { admin }
context 'when unauthenticated' do
@@ -485,8 +520,9 @@ describe API::API, api: true do
it "does not create invalid email" do
post api("/users/#{user.id}/emails", admin), {}
+
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('400 (Bad request) "email" not given')
+ expect(json_response['error']).to eq('email is missing')
end
it "creates email" do
@@ -503,7 +539,7 @@ describe API::API, api: true do
end
end
- describe 'GET /user/:uid/emails' do
+ describe 'GET /user/:id/emails' do
before { admin }
context 'when unauthenticated' do
@@ -537,7 +573,7 @@ describe API::API, api: true do
end
end
- describe 'DELETE /user/:uid/emails/:id' do
+ describe 'DELETE /user/:id/emails/:email_id' do
before { admin }
context 'when unauthenticated' do
@@ -615,20 +651,78 @@ describe API::API, api: true do
end
describe "GET /user" do
- it "returns current user" do
- get api("/user", user)
- expect(response).to have_http_status(200)
- expect(json_response['email']).to eq(user.email)
- expect(json_response['is_admin']).to eq(user.is_admin?)
- expect(json_response['can_create_project']).to eq(user.can_create_project?)
- expect(json_response['can_create_group']).to eq(user.can_create_group?)
- expect(json_response['projects_limit']).to eq(user.projects_limit)
- expect(json_response['private_token']).to be_blank
+ let(:personal_access_token) { create(:personal_access_token, user: user).token }
+
+ context 'with regular user' do
+ context 'with personal access token' do
+ it 'returns 403 without private token when sudo is defined' do
+ get api("/user?private_token=#{personal_access_token}&sudo=123")
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'with private token' do
+ it 'returns 403 without private token when sudo defined' do
+ get api("/user?private_token=#{user.private_token}&sudo=123")
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ it 'returns current user without private token when sudo not defined' do
+ get api("/user", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('user/public')
+ expect(json_response['id']).to eq(user.id)
+ end
end
- it "returns 401 error if user is unauthenticated" do
- get api("/user")
- expect(response).to have_http_status(401)
+ context 'with admin' do
+ let(:admin_personal_access_token) { create(:personal_access_token, user: admin).token }
+
+ context 'with personal access token' do
+ it 'returns 403 without private token when sudo defined' do
+ get api("/user?private_token=#{admin_personal_access_token}&sudo=#{user.id}")
+
+ expect(response).to have_http_status(403)
+ end
+
+ it 'returns initial current user without private token when sudo not defined' do
+ get api("/user?private_token=#{admin_personal_access_token}")
+
+ expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('user/public')
+ expect(json_response['id']).to eq(admin.id)
+ end
+ end
+
+ context 'with private token' do
+ it 'returns sudoed user with private token when sudo defined' do
+ get api("/user?private_token=#{admin.private_token}&sudo=#{user.id}")
+
+ expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('user/login')
+ expect(json_response['id']).to eq(user.id)
+ end
+
+ it 'returns initial current user without private token when sudo not defined' do
+ get api("/user?private_token=#{admin.private_token}")
+
+ expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('user/public')
+ expect(json_response['id']).to eq(admin.id)
+ end
+ end
+ end
+
+ context 'with unauthenticated user' do
+ it "returns 401 error if user is unauthenticated" do
+ get api("/user")
+
+ expect(response).to have_http_status(401)
+ end
end
end
@@ -652,7 +746,7 @@ describe API::API, api: true do
end
end
- describe "GET /user/keys/:id" do
+ describe "GET /user/keys/:key_id" do
it "returns single key" do
user.keys << key
user.save
@@ -665,7 +759,7 @@ describe API::API, api: true do
get api("/user/keys/42", user)
expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Not found')
+ expect(json_response['message']).to eq('404 Key Not Found')
end
it "returns 404 error if admin accesses user's ssh key" do
@@ -674,7 +768,7 @@ describe API::API, api: true do
admin
get api("/user/keys/#{key.id}", admin)
expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Not found')
+ expect(json_response['message']).to eq('404 Key Not Found')
end
it "returns 404 for invalid ID" do
@@ -700,14 +794,16 @@ describe API::API, api: true do
it "does not create ssh key without key" do
post api("/user/keys", user), title: 'title'
+
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('400 (Bad request) "key" not given')
+ expect(json_response['error']).to eq('key is missing')
end
it 'does not create ssh key without title' do
post api('/user/keys', user), key: 'some key'
+
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('400 (Bad request) "title" not given')
+ expect(json_response['error']).to eq('title is missing')
end
it "does not create ssh key without title" do
@@ -716,7 +812,7 @@ describe API::API, api: true do
end
end
- describe "DELETE /user/keys/:id" do
+ describe "DELETE /user/keys/:key_id" do
it "deletes existed key" do
user.keys << key
user.save
@@ -726,9 +822,11 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
end
- it "returns success if key ID not found" do
+ it "returns 404 if key ID not found" do
delete api("/user/keys/42", user)
- expect(response).to have_http_status(200)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Key Not Found')
end
it "returns 401 error if unauthorized" do
@@ -765,7 +863,7 @@ describe API::API, api: true do
end
end
- describe "GET /user/emails/:id" do
+ describe "GET /user/emails/:email_id" do
it "returns single email" do
user.emails << email
user.save
@@ -777,7 +875,7 @@ describe API::API, api: true do
it "returns 404 Not Found within invalid ID" do
get api("/user/emails/42", user)
expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Not found')
+ expect(json_response['message']).to eq('404 Email Not Found')
end
it "returns 404 error if admin accesses user's email" do
@@ -786,7 +884,7 @@ describe API::API, api: true do
admin
get api("/user/emails/#{email.id}", admin)
expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Not found')
+ expect(json_response['message']).to eq('404 Email Not Found')
end
it "returns 404 for invalid ID" do
@@ -812,12 +910,13 @@ describe API::API, api: true do
it "does not create email with invalid email" do
post api("/user/emails", user), {}
+
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('400 (Bad request) "email" not given')
+ expect(json_response['error']).to eq('email is missing')
end
end
- describe "DELETE /user/emails/:id" do
+ describe "DELETE /user/emails/:email_id" do
it "deletes existed email" do
user.emails << email
user.save
@@ -827,9 +926,11 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
end
- it "returns success if email ID not found" do
+ it "returns 404 if email ID not found" do
delete api("/user/emails/42", user)
- expect(response).to have_http_status(200)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Email Not Found')
end
it "returns 401 error if unauthorized" do
@@ -839,10 +940,10 @@ describe API::API, api: true do
expect(response).to have_http_status(401)
end
- it "returns a 404 for invalid ID" do
- delete api("/users/emails/ASDF", admin)
+ it "returns 400 for invalid ID" do
+ delete api("/user/emails/ASDF", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_http_status(400)
end
end
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index 05fbdb909dc..7435f320607 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::Variables, api: true do
include ApiHelpers
let(:user) { create(:user) }
diff --git a/spec/requests/api/version_spec.rb b/spec/requests/api/version_spec.rb
index 54b69a0cae7..da1b2fda70e 100644
--- a/spec/requests/api/version_spec.rb
+++ b/spec/requests/api/version_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::Version, api: true do
include ApiHelpers
describe 'GET /version' do
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index 6d49c42c215..80652129928 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Ci::API::API do
+describe Ci::API::Builds do
include ApiHelpers
let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) }
@@ -17,6 +17,10 @@ describe Ci::API::API do
let!(:build) { create(:ci_build, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let(:user_agent) { 'gitlab-ci-multi-runner 1.5.2 (1-5-stable; go1.6.3; linux/amd64)' }
+ before do
+ stub_container_registry_config(enabled: false)
+ end
+
shared_examples 'no builds available' do
context 'when runner sends version in User-Agent' do
context 'for stable version' do
@@ -53,6 +57,41 @@ describe Ci::API::API do
it 'updates runner info' do
expect { register_builds }.to change { runner.reload.contacted_at }
end
+
+ context 'registry credentials' do
+ let(:registry_credentials) do
+ { 'type' => 'registry',
+ 'url' => 'registry.example.com:5005',
+ 'username' => 'gitlab-ci-token',
+ 'password' => build.token }
+ end
+
+ context 'when registry is enabled' do
+ before do
+ stub_container_registry_config(enabled: true, host_port: 'registry.example.com:5005')
+ end
+
+ it 'sends registry credentials key' do
+ register_builds info: { platform: :darwin }
+
+ expect(json_response).to have_key('credentials')
+ expect(json_response['credentials']).to include(registry_credentials)
+ end
+ end
+
+ context 'when registry is disabled' do
+ before do
+ stub_container_registry_config(enabled: false, host_port: 'registry.example.com:5005')
+ end
+
+ it 'does not send registry credentials' do
+ register_builds info: { platform: :darwin }
+
+ expect(json_response).to have_key('credentials')
+ expect(json_response['credentials']).not_to include(registry_credentials)
+ end
+ end
+ end
end
context 'when builds are finished' do
@@ -213,26 +252,102 @@ describe Ci::API::API do
let(:build) { create(:ci_build, :pending, :trace, runner_id: runner.id) }
let(:headers) { { Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token, 'Content-Type' => 'text/plain' } }
let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) }
+ let(:update_interval) { 10.seconds.to_i }
+
+ def patch_the_trace(content = ' appended', request_headers = nil)
+ unless request_headers
+ offset = build.trace_length
+ limit = offset + content.length - 1
+ request_headers = headers.merge({ 'Content-Range' => "#{offset}-#{limit}" })
+ end
+
+ Timecop.travel(build.updated_at + update_interval) do
+ patch ci_api("/builds/#{build.id}/trace.txt"), content, request_headers
+ build.reload
+ end
+ end
+
+ def initial_patch_the_trace
+ patch_the_trace(' appended', headers_with_range)
+ end
+
+ def force_patch_the_trace
+ 2.times { patch_the_trace('') }
+ end
before do
build.run!
- patch ci_api("/builds/#{build.id}/trace.txt"), ' appended', headers_with_range
+ initial_patch_the_trace
end
context 'when request is valid' do
it 'gets correct response' do
expect(response.status).to eq 202
+ expect(build.reload.trace).to eq 'BUILD TRACE appended'
expect(response.header).to have_key 'Range'
expect(response.header).to have_key 'Build-Status'
end
- it { expect(build.reload.trace).to eq 'BUILD TRACE appended' }
+ context 'when build has been updated recently' do
+ it { expect{ patch_the_trace }.not_to change { build.updated_at }}
+
+ it 'changes the build trace' do
+ patch_the_trace
+
+ expect(build.reload.trace).to eq 'BUILD TRACE appended appended'
+ end
+
+ context 'when Runner makes a force-patch' do
+ it { expect{ force_patch_the_trace }.not_to change { build.updated_at }}
+
+ it "doesn't change the build.trace" do
+ force_patch_the_trace
+
+ expect(build.reload.trace).to eq 'BUILD TRACE appended'
+ end
+ end
+ end
+
+ context 'when build was not updated recently' do
+ let(:update_interval) { 15.minutes.to_i }
+
+ it { expect { patch_the_trace }.to change { build.updated_at } }
+
+ it 'changes the build.trace' do
+ patch_the_trace
+
+ expect(build.reload.trace).to eq 'BUILD TRACE appended appended'
+ end
+
+ context 'when Runner makes a force-patch' do
+ it { expect { force_patch_the_trace }.to change { build.updated_at } }
+
+ it "doesn't change the build.trace" do
+ force_patch_the_trace
+
+ expect(build.reload.trace).to eq 'BUILD TRACE appended'
+ end
+ end
+ end
+ end
+
+ context 'when Runner makes a force-patch' do
+ before do
+ force_patch_the_trace
+ end
+
+ it 'gets correct response' do
+ expect(response.status).to eq 202
+ expect(build.reload.trace).to eq 'BUILD TRACE appended'
+ expect(response.header).to have_key 'Range'
+ expect(response.header).to have_key 'Build-Status'
+ end
end
context 'when content-range start is too big' do
let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20' }) }
- it 'gets correct response' do
+ it 'gets 416 error response with range headers' do
expect(response.status).to eq 416
expect(response.header).to have_key 'Range'
expect(response.header['Range']).to eq '0-11'
@@ -242,7 +357,7 @@ describe Ci::API::API do
context 'when content-range start is too small' do
let(:headers_with_range) { headers.merge({ 'Content-Range' => '8-20' }) }
- it 'gets correct response' do
+ it 'gets 416 error response with range headers' do
expect(response.status).to eq 416
expect(response.header).to have_key 'Range'
expect(response.header['Range']).to eq '0-11'
@@ -250,7 +365,7 @@ describe Ci::API::API do
end
context 'when Content-Range header is missing' do
- let(:headers_with_range) { headers.merge({}) }
+ let(:headers_with_range) { headers }
it { expect(response.status).to eq 400 }
end
diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb
index d6c26fd8a94..bd55934d0c8 100644
--- a/spec/requests/ci/api/runners_spec.rb
+++ b/spec/requests/ci/api/runners_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Ci::API::API do
+describe Ci::API::Runners do
include ApiHelpers
include StubGitlabCalls
diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb
index 0a0f979f57d..2d434ab5dd8 100644
--- a/spec/requests/ci/api/triggers_spec.rb
+++ b/spec/requests/ci/api/triggers_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Ci::API::API do
+describe Ci::API::Triggers do
include ApiHelpers
describe 'POST /projects/:project_id/refs/:ref/trigger' do
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 27f0fd22ae6..d71bb08c218 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -115,6 +115,38 @@ describe 'Git HTTP requests', lib: true do
end.to raise_error(JWT::DecodeError)
end
end
+
+ context 'when the repo is public' do
+ context 'but the repo is disabled' do
+ it 'does not allow to clone the repo' do
+ project = create(:project, :public, repository_access_level: ProjectFeature::DISABLED)
+
+ download("#{project.path_with_namespace}.git", {}) do |response|
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ end
+
+ context 'but the repo is enabled' do
+ it 'allows to clone the repo' do
+ project = create(:project, :public, repository_access_level: ProjectFeature::ENABLED)
+
+ download("#{project.path_with_namespace}.git", {}) do |response|
+ expect(response).to have_http_status(:ok)
+ end
+ end
+ end
+
+ context 'but only project members are allowed' do
+ it 'does not allow to clone the repo' do
+ project = create(:project, :public, repository_access_level: ProjectFeature::PRIVATE)
+
+ download("#{project.path_with_namespace}.git", {}) do |response|
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ end
+ end
end
context "when the project is private" do
@@ -198,7 +230,7 @@ describe 'Git HTTP requests', lib: true do
context "when an oauth token is provided" do
before do
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)
+ @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api")
end
it "downloads get status 200" do
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index f0ef155bd7b..a3e7844b2f3 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -20,7 +20,7 @@ describe JwtController do
end
end
- context 'when using authorized request' do
+ context 'when using authenticated request' do
context 'using CI token' do
let(:build) { create(:ci_build, :running) }
let(:project) { build.project }
@@ -65,7 +65,7 @@ describe JwtController do
let(:access_token) { create(:personal_access_token, user: user) }
let(:headers) { { authorization: credentials(user.username, access_token.token) } }
- it 'rejects the authorization attempt' do
+ it 'accepts the authorization attempt' do
expect(response).to have_http_status(200)
end
end
@@ -81,6 +81,20 @@ describe JwtController do
end
end
+ context 'when using unauthenticated request' do
+ it 'accepts the authorization attempt' do
+ get '/jwt/auth', parameters
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'allows read access' do
+ expect(service).to receive(:execute).with(authentication_abilities: Gitlab::Auth.read_authentication_abilities)
+
+ get '/jwt/auth', parameters
+ end
+ end
+
context 'unknown service' do
subject! { get '/jwt/auth', service: 'unknown' }
diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb
new file mode 100644
index 00000000000..e0368e6001f
--- /dev/null
+++ b/spec/requests/projects/cycle_analytics_events_spec.rb
@@ -0,0 +1,140 @@
+require 'spec_helper'
+
+describe 'cycle analytics events' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, public_builds: false) }
+ let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
+
+ describe 'GET /:namespace/:project/cycle_analytics/events/issues' do
+ before do
+ project.team << [user, :developer]
+
+ allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
+
+ 3.times { create_cycle }
+ deploy_master
+
+ login_as(user)
+ end
+
+ it 'lists the issue events' do
+ get namespace_project_cycle_analytics_issue_path(project.namespace, project, format: :json)
+
+ expect(json_response['events']).not_to be_empty
+
+ first_issue_iid = Issue.order(created_at: :desc).pluck(:iid).first.to_s
+
+ expect(json_response['events'].first['iid']).to eq(first_issue_iid)
+ end
+
+ it 'lists the plan events' do
+ get namespace_project_cycle_analytics_plan_path(project.namespace, project, format: :json)
+
+ expect(json_response['events']).not_to be_empty
+
+ expect(json_response['events'].first['short_sha']).to eq(MergeRequest.last.commits.first.short_id)
+ end
+
+ it 'lists the code events' do
+ get namespace_project_cycle_analytics_code_path(project.namespace, project, format: :json)
+
+ expect(json_response['events']).not_to be_empty
+
+ first_mr_iid = project.merge_requests.order(id: :desc).pluck(:iid).first.to_s
+
+ expect(json_response['events'].first['iid']).to eq(first_mr_iid)
+ end
+
+ it 'lists the test events' do
+ get namespace_project_cycle_analytics_test_path(project.namespace, project, format: :json)
+
+ expect(json_response['events']).not_to be_empty
+
+ expect(json_response['events'].first['date']).not_to be_empty
+ end
+
+ it 'lists the review events' do
+ get namespace_project_cycle_analytics_review_path(project.namespace, project, format: :json)
+
+ expect(json_response['events']).not_to be_empty
+
+ first_mr_iid = MergeRequest.order(created_at: :desc).pluck(:iid).first.to_s
+
+ expect(json_response['events'].first['iid']).to eq(first_mr_iid)
+ end
+
+ it 'lists the staging events' do
+ get namespace_project_cycle_analytics_staging_path(project.namespace, project, format: :json)
+
+ expect(json_response['events']).not_to be_empty
+
+ expect(json_response['events'].first['date']).not_to be_empty
+ end
+
+ it 'lists the production events' do
+ get namespace_project_cycle_analytics_production_path(project.namespace, project, format: :json)
+
+ expect(json_response['events']).not_to be_empty
+
+ first_issue_iid = Issue.order(created_at: :desc).pluck(:iid).first.to_s
+
+ expect(json_response['events'].first['iid']).to eq(first_issue_iid)
+ end
+
+ context 'specific branch' do
+ it 'lists the test events' do
+ branch = MergeRequest.first.source_branch
+
+ get namespace_project_cycle_analytics_test_path(project.namespace, project, format: :json, branch: branch)
+
+ expect(json_response['events']).not_to be_empty
+
+ expect(json_response['events'].first['date']).not_to be_empty
+ end
+ end
+
+ context 'with private project and builds' do
+ before do
+ ProjectMember.first.update(access_level: Gitlab::Access::GUEST)
+ end
+
+ it 'does not list the test events' do
+ get namespace_project_cycle_analytics_test_path(project.namespace, project, format: :json)
+
+ expect(response).to have_http_status(:not_found)
+ end
+
+ it 'does not list the staging events' do
+ get namespace_project_cycle_analytics_staging_path(project.namespace, project, format: :json)
+
+ expect(response).to have_http_status(:not_found)
+ end
+
+ it 'lists the issue events' do
+ get namespace_project_cycle_analytics_issue_path(project.namespace, project, format: :json)
+
+ expect(response).to have_http_status(:ok)
+ end
+ end
+ end
+
+ def json_response
+ JSON.parse(response.body)
+ end
+
+ def create_cycle
+ milestone = create(:milestone, project: project)
+ issue.update(milestone: milestone)
+ mr = create_merge_request_closing_issue(issue)
+
+ pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha)
+ pipeline.run
+
+ create(:ci_build, pipeline: pipeline, status: :success, author: user)
+ create(:ci_build, pipeline: pipeline, status: :success, author: user)
+
+ merge_merge_requests_closing_issue(issue)
+
+ ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
+ end
+end
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index 69eeb45ed71..661b671301e 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -66,7 +66,8 @@ describe Admin::ProjectsController, "routing" do
end
it "to #show" do
- expect(get("/admin/projects/gitlab")).to route_to('admin/projects#show', namespace_id: 'gitlab')
+ expect(get("/admin/projects/gitlab/gitlab-ce")).to route_to('admin/projects#show', namespace_id: 'gitlab', id: 'gitlab-ce')
+ expect(get("/admin/projects/gitlab/subgroup/gitlab-ce")).to route_to('admin/projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlab-ce')
end
end
@@ -119,3 +120,14 @@ describe Admin::HealthCheckController, "routing" do
expect(get("/admin/health_check")).to route_to('admin/health_check#show')
end
end
+
+describe Admin::GroupsController, "routing" do
+ it "to #index" do
+ expect(get("/admin/groups")).to route_to('admin/groups#index')
+ end
+
+ it "to #show" do
+ expect(get("/admin/groups/gitlab")).to route_to('admin/groups#show', id: 'gitlab')
+ expect(get("/admin/groups/gitlab/subgroup")).to route_to('admin/groups#show', id: 'gitlab/subgroup')
+ end
+end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 2322430d212..77549db2927 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -1,511 +1,542 @@
require 'spec_helper'
-# Shared examples for a resource inside a Project
-#
-# By default it tests all the default REST actions: index, create, new, edit,
-# show, update, and destroy. You can remove actions by customizing the
-# `actions` variable.
-#
-# It also expects a `controller` variable to be available which defines both
-# the path to the resource as well as the controller name.
-#
-# Examples
-#
-# # Default behavior
-# it_behaves_like 'RESTful project resources' do
-# let(:controller) { 'issues' }
-# end
-#
-# # Customizing actions
-# it_behaves_like 'RESTful project resources' do
-# let(:actions) { [:index] }
-# let(:controller) { 'issues' }
-# end
-shared_examples 'RESTful project resources' do
- let(:actions) { [:index, :create, :new, :edit, :show, :update, :destroy] }
-
- 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)
- 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)
- 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)
- 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)
- 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)
- 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)
- 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)
- end
-end
-
-# projects POST /projects(.:format) projects#create
-# new_project GET /projects/new(.:format) projects#new
-# files_project GET /:id/files(.:format) projects#files
-# edit_project GET /:id/edit(.:format) projects#edit
-# project GET /:id(.:format) projects#show
-# PUT /:id(.:format) projects#update
-# DELETE /:id(.:format) projects#destroy
-# preview_markdown_project POST /:id/preview_markdown(.:format) projects#preview_markdown
-describe ProjectsController, 'routing' do
- it 'to #create' do
- expect(post('/projects')).to route_to('projects#create')
- end
-
- it 'to #new' do
- expect(get('/projects/new')).to route_to('projects#new')
- end
-
- it 'to #edit' do
- expect(get('/gitlab/gitlabhq/edit')).to route_to('projects#edit', namespace_id: 'gitlab', id: 'gitlabhq')
- end
-
- it 'to #autocomplete_sources' do
- expect(get('/gitlab/gitlabhq/autocomplete_sources')).to route_to('projects#autocomplete_sources', namespace_id: 'gitlab', id: 'gitlabhq')
- end
-
- it 'to #show' do
- expect(get('/gitlab/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq')
- expect(get('/gitlab/gitlabhq.keys')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq.keys')
- end
-
- it 'to #update' do
- expect(put('/gitlab/gitlabhq')).to route_to('projects#update', namespace_id: 'gitlab', id: 'gitlabhq')
- end
-
- it 'to #destroy' do
- expect(delete('/gitlab/gitlabhq')).to route_to('projects#destroy', namespace_id: 'gitlab', id: 'gitlabhq')
- end
-
- it 'to #preview_markdown' do
- expect(post('/gitlab/gitlabhq/preview_markdown')).to(
- route_to('projects#preview_markdown', namespace_id: 'gitlab', id: 'gitlabhq')
- )
- end
-end
-
-# pages_project_wikis GET /:project_id/wikis/pages(.:format) projects/wikis#pages
-# history_project_wiki GET /:project_id/wikis/:id/history(.:format) projects/wikis#history
-# project_wikis POST /:project_id/wikis(.:format) projects/wikis#create
-# edit_project_wiki GET /:project_id/wikis/:id/edit(.:format) projects/wikis#edit
-# project_wiki GET /:project_id/wikis/:id(.:format) projects/wikis#show
-# DELETE /:project_id/wikis/:id(.:format) projects/wikis#destroy
-describe Projects::WikisController, 'routing' do
- it 'to #pages' do
- expect(get('/gitlab/gitlabhq/wikis/pages')).to route_to('projects/wikis#pages', namespace_id: 'gitlab', project_id: 'gitlabhq')
- end
-
- it 'to #history' do
- expect(get('/gitlab/gitlabhq/wikis/1/history')).to route_to('projects/wikis#history', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
- end
-
- it_behaves_like 'RESTful project resources' do
- let(:actions) { [:create, :edit, :show, :destroy] }
- let(:controller) { 'wikis' }
- end
-end
-
-# branches_project_repository GET /:project_id/repository/branches(.:format) projects/repositories#branches
-# tags_project_repository GET /:project_id/repository/tags(.:format) projects/repositories#tags
-# archive_project_repository GET /:project_id/repository/archive(.:format) projects/repositories#archive
-# edit_project_repository GET /:project_id/repository/edit(.:format) projects/repositories#edit
-describe Projects::RepositoriesController, 'routing' do
- it 'to #archive' do
- expect(get('/gitlab/gitlabhq/repository/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq')
- end
-
- it 'to #archive format:zip' do
- expect(get('/gitlab/gitlabhq/repository/archive.zip')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'zip')
- end
-
- it 'to #archive format:tar.bz2' do
- expect(get('/gitlab/gitlabhq/repository/archive.tar.bz2')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.bz2')
- end
-end
-
-describe Projects::BranchesController, 'routing' do
- it 'to #branches' do
- expect(get('/gitlab/gitlabhq/branches')).to route_to('projects/branches#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
- expect(delete('/gitlab/gitlabhq/branches/feature%2345')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45')
- expect(delete('/gitlab/gitlabhq/branches/feature%2B45')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45')
- expect(delete('/gitlab/gitlabhq/branches/feature@45')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45')
- expect(delete('/gitlab/gitlabhq/branches/feature%2345/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45/foo/bar/baz')
- expect(delete('/gitlab/gitlabhq/branches/feature%2B45/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45/foo/bar/baz')
- expect(delete('/gitlab/gitlabhq/branches/feature@45/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45/foo/bar/baz')
- end
-end
-
-describe Projects::TagsController, 'routing' do
- it 'to #tags' do
- expect(get('/gitlab/gitlabhq/tags')).to route_to('projects/tags#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
- expect(delete('/gitlab/gitlabhq/tags/feature%2345')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45')
- expect(delete('/gitlab/gitlabhq/tags/feature%2B45')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45')
- expect(delete('/gitlab/gitlabhq/tags/feature@45')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45')
- expect(delete('/gitlab/gitlabhq/tags/feature%2345/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45/foo/bar/baz')
- expect(delete('/gitlab/gitlabhq/tags/feature%2B45/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45/foo/bar/baz')
- expect(delete('/gitlab/gitlabhq/tags/feature@45/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45/foo/bar/baz')
- end
-end
-
-# project_deploy_keys GET /:project_id/deploy_keys(.:format) deploy_keys#index
-# POST /:project_id/deploy_keys(.:format) deploy_keys#create
-# new_project_deploy_key GET /:project_id/deploy_keys/new(.:format) deploy_keys#new
-# project_deploy_key GET /:project_id/deploy_keys/:id(.:format) deploy_keys#show
-# DELETE /:project_id/deploy_keys/:id(.:format) deploy_keys#destroy
-describe Projects::DeployKeysController, 'routing' do
- it_behaves_like 'RESTful project resources' do
- let(:actions) { [:index, :new, :create] }
- let(:controller) { 'deploy_keys' }
- end
-end
-
-# project_protected_branches GET /:project_id/protected_branches(.:format) protected_branches#index
-# POST /:project_id/protected_branches(.:format) protected_branches#create
-# project_protected_branch DELETE /:project_id/protected_branches/:id(.:format) protected_branches#destroy
-describe Projects::ProtectedBranchesController, 'routing' do
- it_behaves_like 'RESTful project resources' do
- let(:actions) { [:index, :create, :destroy] }
- let(:controller) { 'protected_branches' }
- end
-end
-
-# switch_project_refs GET /:project_id/refs/switch(.:format) refs#switch
-# logs_tree_project_ref GET /:project_id/refs/:id/logs_tree(.:format) refs#logs_tree
-# logs_file_project_ref GET /:project_id/refs/:id/logs_tree/:path(.:format) refs#logs_tree
-describe Projects::RefsController, 'routing' do
- it 'to #switch' do
- expect(get('/gitlab/gitlabhq/refs/switch')).to route_to('projects/refs#switch', namespace_id: 'gitlab', project_id: 'gitlabhq')
- end
-
- it 'to #logs_tree' do
- expect(get('/gitlab/gitlabhq/refs/stable/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'stable')
- expect(get('/gitlab/gitlabhq/refs/feature%2345/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45')
- expect(get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45')
- expect(get('/gitlab/gitlabhq/refs/feature@45/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45')
- expect(get('/gitlab/gitlabhq/refs/stable/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'stable', path: 'foo/bar/baz')
- expect(get('/gitlab/gitlabhq/refs/feature%2345/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45', path: 'foo/bar/baz')
- expect(get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45', path: 'foo/bar/baz')
- expect(get('/gitlab/gitlabhq/refs/feature@45/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45', path: 'foo/bar/baz')
- expect(get('/gitlab/gitlabhq/refs/stable/logs_tree/files.scss')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'stable', path: 'files.scss')
- end
-end
-
-# diffs_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/diffs(.:format) projects/merge_requests#diffs
-# commits_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/commits(.:format) projects/merge_requests#commits
-# merge_namespace_project_merge_request POST /:namespace_id/:project_id/merge_requests/:id/merge(.:format) projects/merge_requests#merge
-# merge_check_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/merge_check(.:format) projects/merge_requests#merge_check
-# ci_status_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/ci_status(.:format) projects/merge_requests#ci_status
-# toggle_subscription_namespace_project_merge_request POST /:namespace_id/:project_id/merge_requests/:id/toggle_subscription(.:format) projects/merge_requests#toggle_subscription
-# branch_from_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/branch_from(.:format) projects/merge_requests#branch_from
-# branch_to_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/branch_to(.:format) projects/merge_requests#branch_to
-# update_branches_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/update_branches(.:format) projects/merge_requests#update_branches
-# namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests(.:format) projects/merge_requests#index
-# POST /:namespace_id/:project_id/merge_requests(.:format) projects/merge_requests#create
-# new_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/new(.:format) projects/merge_requests#new
-# edit_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/edit(.:format) projects/merge_requests#edit
-# namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#show
-# PATCH /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#update
-# PUT /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#update
-describe Projects::MergeRequestsController, 'routing' do
- it 'to #diffs' do
- expect(get('/gitlab/gitlabhq/merge_requests/1/diffs')).to route_to('projects/merge_requests#diffs', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
- end
-
- it 'to #commits' do
- expect(get('/gitlab/gitlabhq/merge_requests/1/commits')).to route_to('projects/merge_requests#commits', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
- end
-
- it 'to #merge' do
- expect(post('/gitlab/gitlabhq/merge_requests/1/merge')).to route_to(
- 'projects/merge_requests#merge',
- namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1'
- )
- end
-
- it 'to #merge_check' do
- expect(get('/gitlab/gitlabhq/merge_requests/1/merge_check')).to route_to('projects/merge_requests#merge_check', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
- end
-
- it 'to #branch_from' do
- expect(get('/gitlab/gitlabhq/merge_requests/branch_from')).to route_to('projects/merge_requests#branch_from', namespace_id: 'gitlab', project_id: 'gitlabhq')
- end
-
- it 'to #branch_to' do
- expect(get('/gitlab/gitlabhq/merge_requests/branch_to')).to route_to('projects/merge_requests#branch_to', namespace_id: 'gitlab', project_id: 'gitlabhq')
- end
-
- it 'to #show' do
- expect(get('/gitlab/gitlabhq/merge_requests/1.diff')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'diff')
- expect(get('/gitlab/gitlabhq/merge_requests/1.patch')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'patch')
- end
-
- it_behaves_like 'RESTful project resources' do
- let(:controller) { 'merge_requests' }
- let(:actions) { [:index, :create, :new, :edit, :show, :update] }
- end
-end
-
-# raw_project_snippet GET /:project_id/snippets/:id/raw(.:format) snippets#raw
-# project_snippets GET /:project_id/snippets(.:format) snippets#index
-# POST /:project_id/snippets(.:format) snippets#create
-# new_project_snippet GET /:project_id/snippets/new(.:format) snippets#new
-# edit_project_snippet GET /:project_id/snippets/:id/edit(.:format) snippets#edit
-# project_snippet GET /:project_id/snippets/:id(.:format) snippets#show
-# PUT /:project_id/snippets/:id(.:format) snippets#update
-# DELETE /:project_id/snippets/:id(.:format) snippets#destroy
-describe SnippetsController, 'routing' do
- it 'to #raw' do
- expect(get('/gitlab/gitlabhq/snippets/1/raw')).to route_to('projects/snippets#raw', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
- end
-
- it 'to #index' do
- expect(get('/gitlab/gitlabhq/snippets')).to route_to('projects/snippets#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
- end
-
- it 'to #create' do
- expect(post('/gitlab/gitlabhq/snippets')).to route_to('projects/snippets#create', namespace_id: 'gitlab', project_id: 'gitlabhq')
- end
-
- it 'to #new' do
- expect(get('/gitlab/gitlabhq/snippets/new')).to route_to('projects/snippets#new', namespace_id: 'gitlab', project_id: 'gitlabhq')
- end
-
- it 'to #edit' do
- expect(get('/gitlab/gitlabhq/snippets/1/edit')).to route_to('projects/snippets#edit', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
- end
-
- it 'to #show' do
- expect(get('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
- end
-
- it 'to #update' do
- expect(put('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#update', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
- end
-
- it 'to #destroy' do
- expect(delete('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
- end
-end
-
-# test_project_hook GET /:project_id/hooks/:id/test(.:format) hooks#test
-# project_hooks GET /:project_id/hooks(.:format) hooks#index
-# POST /:project_id/hooks(.:format) hooks#create
-# project_hook DELETE /:project_id/hooks/:id(.:format) hooks#destroy
-describe Projects::HooksController, 'routing' do
- it 'to #test' do
- expect(get('/gitlab/gitlabhq/hooks/1/test')).to route_to('projects/hooks#test', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
- end
-
- it_behaves_like 'RESTful project resources' do
- let(:actions) { [:index, :create, :destroy] }
- let(:controller) { 'hooks' }
- end
-end
-
-# project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /\h{7,40}/, project_id: /[^\/]+/}
-describe Projects::CommitController, 'routing' do
- it 'to #show' do
- expect(get('/gitlab/gitlabhq/commit/4246fbd')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd')
- expect(get('/gitlab/gitlabhq/commit/4246fbd.diff')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd', format: 'diff')
- expect(get('/gitlab/gitlabhq/commit/4246fbd.patch')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd', format: 'patch')
- expect(get('/gitlab/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5')
- end
-end
-
-# patch_project_commit GET /:project_id/commits/:id/patch(.:format) commits#patch
-# project_commits GET /:project_id/commits(.:format) commits#index
-# POST /:project_id/commits(.:format) commits#create
-# project_commit GET /:project_id/commits/:id(.:format) commits#show
-describe Projects::CommitsController, 'routing' do
- it_behaves_like 'RESTful project resources' do
- let(:actions) { [:show] }
- let(:controller) { 'commits' }
- end
-
- it 'to #show' do
- expect(get('/gitlab/gitlabhq/commits/master.atom')).to route_to('projects/commits#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master.atom')
- end
-end
-
-# project_project_members GET /:project_id/project_members(.:format) project_members#index
-# POST /:project_id/project_members(.:format) project_members#create
-# PUT /:project_id/project_members/:id(.:format) project_members#update
-# DELETE /:project_id/project_members/:id(.:format) project_members#destroy
-describe Projects::ProjectMembersController, 'routing' do
- it_behaves_like 'RESTful project resources' do
- let(:actions) { [:index, :create, :update, :destroy] }
- let(:controller) { 'project_members' }
- end
-end
-
-# project_milestones GET /:project_id/milestones(.:format) milestones#index
-# POST /:project_id/milestones(.:format) milestones#create
-# new_project_milestone GET /:project_id/milestones/new(.:format) milestones#new
-# edit_project_milestone GET /:project_id/milestones/:id/edit(.:format) milestones#edit
-# project_milestone GET /:project_id/milestones/:id(.:format) milestones#show
-# PUT /:project_id/milestones/:id(.:format) milestones#update
-# DELETE /:project_id/milestones/:id(.:format) milestones#destroy
-describe Projects::MilestonesController, 'routing' do
- it_behaves_like 'RESTful project resources' do
- let(:controller) { 'milestones' }
- let(:actions) { [:index, :create, :new, :edit, :show, :update] }
- end
-end
-
-# project_labels GET /:project_id/labels(.:format) labels#index
-describe Projects::LabelsController, 'routing' do
- it 'to #index' do
- expect(get('/gitlab/gitlabhq/labels')).to route_to('projects/labels#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
- end
-end
-
-# sort_project_issues POST /:project_id/issues/sort(.:format) issues#sort
-# bulk_update_project_issues POST /:project_id/issues/bulk_update(.:format) issues#bulk_update
-# search_project_issues GET /:project_id/issues/search(.:format) issues#search
-# project_issues GET /:project_id/issues(.:format) issues#index
-# POST /:project_id/issues(.:format) issues#create
-# new_project_issue GET /:project_id/issues/new(.:format) issues#new
-# edit_project_issue GET /:project_id/issues/:id/edit(.:format) issues#edit
-# project_issue GET /:project_id/issues/:id(.:format) issues#show
-# PUT /:project_id/issues/:id(.:format) issues#update
-# DELETE /:project_id/issues/:id(.:format) issues#destroy
-describe Projects::IssuesController, 'routing' do
- it 'to #bulk_update' do
- expect(post('/gitlab/gitlabhq/issues/bulk_update')).to route_to('projects/issues#bulk_update', namespace_id: 'gitlab', project_id: 'gitlabhq')
- end
-
- it_behaves_like 'RESTful project resources' do
- let(:controller) { 'issues' }
- let(:actions) { [:index, :create, :new, :edit, :show, :update] }
- end
-end
-
-# project_notes GET /:project_id/notes(.:format) notes#index
-# POST /:project_id/notes(.:format) notes#create
-# project_note DELETE /:project_id/notes/:id(.:format) notes#destroy
-describe Projects::NotesController, 'routing' do
- it_behaves_like 'RESTful project resources' do
- let(:actions) { [:index, :create, :destroy] }
- let(:controller) { 'notes' }
- end
-end
-
-# project_blame GET /:project_id/blame/:id(.:format) blame#show {id: /.+/, project_id: /[^\/]+/}
-describe Projects::BlameController, 'routing' do
- it 'to #show' do
- expect(get('/gitlab/gitlabhq/blame/master/app/models/project.rb')).to route_to('projects/blame#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb')
- expect(get('/gitlab/gitlabhq/blame/master/files.scss')).to route_to('projects/blame#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss')
- end
-end
-
-# project_blob GET /:project_id/blob/:id(.:format) blob#show {id: /.+/, project_id: /[^\/]+/}
-describe Projects::BlobController, 'routing' do
- it 'to #show' do
- expect(get('/gitlab/gitlabhq/blob/master/app/models/project.rb')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb')
- expect(get('/gitlab/gitlabhq/blob/master/app/models/compare.rb')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/compare.rb')
- expect(get('/gitlab/gitlabhq/blob/master/app/models/diff.js')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/diff.js')
- expect(get('/gitlab/gitlabhq/blob/master/files.scss')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss')
- end
-end
-
-# project_tree GET /:project_id/tree/:id(.:format) tree#show {id: /.+/, project_id: /[^\/]+/}
-describe Projects::TreeController, 'routing' do
- it 'to #show' do
- expect(get('/gitlab/gitlabhq/tree/master/app/models/project.rb')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb')
- expect(get('/gitlab/gitlabhq/tree/master/files.scss')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss')
- end
-end
-
-# project_find_file GET /:namespace_id/:project_id/find_file/*id(.:format) projects/find_file#show {:id=>/.+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/html/}
-# project_files GET /:namespace_id/:project_id/files/*id(.:format) projects/find_file#list {:id=>/(?:[^.]|\.(?!json$))+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/json/}
-describe Projects::FindFileController, 'routing' do
- it 'to #show' do
- expect(get('/gitlab/gitlabhq/find_file/master')).to route_to('projects/find_file#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master')
- end
-
- it 'to #list' do
- expect(get('/gitlab/gitlabhq/files/master.json')).to route_to('projects/find_file#list', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
- end
-end
-
-describe Projects::BlobController, 'routing' do
- it 'to #edit' do
- expect(get('/gitlab/gitlabhq/edit/master/app/models/project.rb')).to(
- route_to('projects/blob#edit',
- namespace_id: 'gitlab', project_id: 'gitlabhq',
- id: 'master/app/models/project.rb'))
- end
-
- it 'to #preview' do
- expect(post('/gitlab/gitlabhq/preview/master/app/models/project.rb')).to(
- route_to('projects/blob#preview',
- namespace_id: 'gitlab', project_id: 'gitlabhq',
- id: 'master/app/models/project.rb'))
- end
-end
-
-# project_compare_index GET /:project_id/compare(.:format) compare#index {id: /[^\/]+/, project_id: /[^\/]+/}
-# POST /:project_id/compare(.:format) compare#create {id: /[^\/]+/, project_id: /[^\/]+/}
-# project_compare /:project_id/compare/:from...:to(.:format) compare#show {from: /.+/, to: /.+/, id: /[^\/]+/, project_id: /[^\/]+/}
-describe Projects::CompareController, 'routing' do
- it 'to #index' do
- expect(get('/gitlab/gitlabhq/compare')).to route_to('projects/compare#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
- end
-
- it 'to #compare' do
- expect(post('/gitlab/gitlabhq/compare')).to route_to('projects/compare#create', namespace_id: 'gitlab', project_id: 'gitlabhq')
- end
-
- it 'to #show' do
- expect(get('/gitlab/gitlabhq/compare/master...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'master', to: 'stable')
- expect(get('/gitlab/gitlabhq/compare/issue/1234...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'issue/1234', to: 'stable')
- end
-end
-
-describe Projects::NetworkController, 'routing' do
- it 'to #show' do
- expect(get('/gitlab/gitlabhq/network/master')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master')
- expect(get('/gitlab/gitlabhq/network/ends-with.json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json')
- expect(get('/gitlab/gitlabhq/network/master?format=json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
- end
-end
-
-describe Projects::GraphsController, 'routing' do
- it 'to #show' do
- expect(get('/gitlab/gitlabhq/graphs/master')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master')
- expect(get('/gitlab/gitlabhq/graphs/ends-with.json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json')
- expect(get('/gitlab/gitlabhq/graphs/master?format=json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
- end
-end
-
-describe Projects::ForksController, 'routing' do
- it 'to #new' do
- expect(get('/gitlab/gitlabhq/forks/new')).to route_to('projects/forks#new', namespace_id: 'gitlab', project_id: 'gitlabhq')
- end
-
- it 'to #create' do
- expect(post('/gitlab/gitlabhq/forks')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq')
- end
-end
-
-# project_avatar DELETE /project/avatar(.:format) projects/avatars#destroy
-describe Projects::AvatarsController, 'routing' do
- it 'to #destroy' do
- expect(delete('/gitlab/gitlabhq/avatar')).to route_to(
- 'projects/avatars#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq')
+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)
+ end
+
+ # Shared examples for a resource inside a Project
+ #
+ # By default it tests all the default REST actions: index, create, new, edit,
+ # show, update, and destroy. You can remove actions by customizing the
+ # `actions` variable.
+ #
+ # It also expects a `controller` variable to be available which defines both
+ # the path to the resource as well as the controller name.
+ #
+ # Examples
+ #
+ # # Default behavior
+ # it_behaves_like 'RESTful project resources' do
+ # let(:controller) { 'issues' }
+ # end
+ #
+ # # Customizing actions
+ # it_behaves_like 'RESTful project resources' do
+ # let(:actions) { [:index] }
+ # let(:controller) { 'issues' }
+ # end
+ shared_examples 'RESTful project resources' do
+ let(:actions) { [:index, :create, :new, :edit, :show, :update, :destroy] }
+
+ 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)
+ 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)
+ 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)
+ 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)
+ 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)
+ 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)
+ 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)
+ end
+ end
+
+ # projects POST /projects(.:format) projects#create
+ # new_project GET /projects/new(.:format) projects#new
+ # files_project GET /:id/files(.:format) projects#files
+ # edit_project GET /:id/edit(.:format) projects#edit
+ # project GET /:id(.:format) projects#show
+ # PUT /:id(.:format) projects#update
+ # DELETE /:id(.:format) projects#destroy
+ # preview_markdown_project POST /:id/preview_markdown(.:format) projects#preview_markdown
+ describe ProjectsController, 'routing' do
+ it 'to #create' do
+ expect(post('/projects')).to route_to('projects#create')
+ end
+
+ it 'to #new' do
+ expect(get('/projects/new')).to route_to('projects#new')
+ end
+
+ it 'to #edit' do
+ expect(get('/gitlab/gitlabhq/edit')).to route_to('projects#edit', namespace_id: 'gitlab', id: 'gitlabhq')
+ end
+
+ describe 'to #show' do
+ context 'regular name' do
+ it { expect(get('/gitlab/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq') }
+ end
+
+ context 'name with dot' do
+ before { allow(Project).to receive(:find_with_namespace).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) }
+
+ it { expect(get('/gitlab/subgroup/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlabhq') }
+ end
+ end
+
+ it 'to #update' do
+ expect(put('/gitlab/gitlabhq')).to route_to('projects#update', namespace_id: 'gitlab', id: 'gitlabhq')
+ end
+
+ it 'to #destroy' do
+ expect(delete('/gitlab/gitlabhq')).to route_to('projects#destroy', namespace_id: 'gitlab', id: 'gitlabhq')
+ end
+
+ it 'to #preview_markdown' do
+ expect(post('/gitlab/gitlabhq/preview_markdown')).to(
+ route_to('projects#preview_markdown', namespace_id: 'gitlab', id: 'gitlabhq')
+ )
+ end
+ end
+
+ # emojis_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/emojis(.:format) projects/autocomplete_sources#emojis
+ # members_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/members(.:format) projects/autocomplete_sources#members
+ # issues_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/issues(.:format) projects/autocomplete_sources#issues
+ # merge_requests_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/merge_requests(.:format) projects/autocomplete_sources#merge_requests
+ # labels_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/labels(.:format) projects/autocomplete_sources#labels
+ # milestones_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/milestones(.:format) projects/autocomplete_sources#milestones
+ # commands_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/commands(.:format) projects/autocomplete_sources#commands
+ describe Projects::AutocompleteSourcesController, 'routing' do
+ [:emojis, :members, :issues, :merge_requests, :labels, :milestones, :commands].each do |action|
+ it "to ##{action}" do
+ expect(get("/gitlab/gitlabhq/autocomplete_sources/#{action}")).to route_to("projects/autocomplete_sources##{action}", namespace_id: 'gitlab', project_id: 'gitlabhq')
+ end
+ end
+ end
+
+ # pages_project_wikis GET /:project_id/wikis/pages(.:format) projects/wikis#pages
+ # history_project_wiki GET /:project_id/wikis/:id/history(.:format) projects/wikis#history
+ # project_wikis POST /:project_id/wikis(.:format) projects/wikis#create
+ # edit_project_wiki GET /:project_id/wikis/:id/edit(.:format) projects/wikis#edit
+ # project_wiki GET /:project_id/wikis/:id(.:format) projects/wikis#show
+ # DELETE /:project_id/wikis/:id(.:format) projects/wikis#destroy
+ describe Projects::WikisController, 'routing' do
+ it 'to #pages' do
+ expect(get('/gitlab/gitlabhq/wikis/pages')).to route_to('projects/wikis#pages', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ end
+
+ it 'to #history' do
+ expect(get('/gitlab/gitlabhq/wikis/1/history')).to route_to('projects/wikis#history', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
+ end
+
+ it_behaves_like 'RESTful project resources' do
+ let(:actions) { [:create, :edit, :show, :destroy] }
+ let(:controller) { 'wikis' }
+ end
+ end
+
+ # branches_project_repository GET /:project_id/repository/branches(.:format) projects/repositories#branches
+ # tags_project_repository GET /:project_id/repository/tags(.:format) projects/repositories#tags
+ # archive_project_repository GET /:project_id/repository/archive(.:format) projects/repositories#archive
+ # edit_project_repository GET /:project_id/repository/edit(.:format) projects/repositories#edit
+ describe Projects::RepositoriesController, 'routing' do
+ it 'to #archive' do
+ expect(get('/gitlab/gitlabhq/repository/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ end
+
+ it 'to #archive format:zip' do
+ expect(get('/gitlab/gitlabhq/repository/archive.zip')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'zip')
+ end
+
+ it 'to #archive format:tar.bz2' do
+ expect(get('/gitlab/gitlabhq/repository/archive.tar.bz2')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.bz2')
+ end
+ end
+
+ describe Projects::BranchesController, 'routing' do
+ it 'to #branches' do
+ expect(get('/gitlab/gitlabhq/branches')).to route_to('projects/branches#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ expect(delete('/gitlab/gitlabhq/branches/feature%2345')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45')
+ expect(delete('/gitlab/gitlabhq/branches/feature%2B45')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45')
+ expect(delete('/gitlab/gitlabhq/branches/feature@45')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45')
+ expect(delete('/gitlab/gitlabhq/branches/feature%2345/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45/foo/bar/baz')
+ expect(delete('/gitlab/gitlabhq/branches/feature%2B45/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45/foo/bar/baz')
+ expect(delete('/gitlab/gitlabhq/branches/feature@45/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45/foo/bar/baz')
+ end
+ end
+
+ describe Projects::TagsController, 'routing' do
+ it 'to #tags' do
+ expect(get('/gitlab/gitlabhq/tags')).to route_to('projects/tags#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ expect(delete('/gitlab/gitlabhq/tags/feature%2345')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45')
+ expect(delete('/gitlab/gitlabhq/tags/feature%2B45')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45')
+ expect(delete('/gitlab/gitlabhq/tags/feature@45')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45')
+ expect(delete('/gitlab/gitlabhq/tags/feature%2345/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45/foo/bar/baz')
+ expect(delete('/gitlab/gitlabhq/tags/feature%2B45/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45/foo/bar/baz')
+ expect(delete('/gitlab/gitlabhq/tags/feature@45/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45/foo/bar/baz')
+ end
+ end
+
+ # project_deploy_keys GET /:project_id/deploy_keys(.:format) deploy_keys#index
+ # POST /:project_id/deploy_keys(.:format) deploy_keys#create
+ # new_project_deploy_key GET /:project_id/deploy_keys/new(.:format) deploy_keys#new
+ # project_deploy_key GET /:project_id/deploy_keys/:id(.:format) deploy_keys#show
+ # DELETE /:project_id/deploy_keys/:id(.:format) deploy_keys#destroy
+ describe Projects::DeployKeysController, 'routing' do
+ it_behaves_like 'RESTful project resources' do
+ let(:actions) { [:index, :new, :create] }
+ let(:controller) { 'deploy_keys' }
+ end
+ end
+
+ # project_protected_branches GET /:project_id/protected_branches(.:format) protected_branches#index
+ # POST /:project_id/protected_branches(.:format) protected_branches#create
+ # project_protected_branch DELETE /:project_id/protected_branches/:id(.:format) protected_branches#destroy
+ describe Projects::ProtectedBranchesController, 'routing' do
+ it_behaves_like 'RESTful project resources' do
+ let(:actions) { [:index, :create, :destroy] }
+ let(:controller) { 'protected_branches' }
+ end
+ end
+
+ # switch_project_refs GET /:project_id/refs/switch(.:format) refs#switch
+ # logs_tree_project_ref GET /:project_id/refs/:id/logs_tree(.:format) refs#logs_tree
+ # logs_file_project_ref GET /:project_id/refs/:id/logs_tree/:path(.:format) refs#logs_tree
+ describe Projects::RefsController, 'routing' do
+ it 'to #switch' do
+ expect(get('/gitlab/gitlabhq/refs/switch')).to route_to('projects/refs#switch', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ end
+
+ it 'to #logs_tree' do
+ expect(get('/gitlab/gitlabhq/refs/stable/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'stable')
+ expect(get('/gitlab/gitlabhq/refs/feature%2345/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45')
+ expect(get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45')
+ expect(get('/gitlab/gitlabhq/refs/feature@45/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45')
+ expect(get('/gitlab/gitlabhq/refs/stable/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'stable', path: 'foo/bar/baz')
+ expect(get('/gitlab/gitlabhq/refs/feature%2345/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45', path: 'foo/bar/baz')
+ expect(get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45', path: 'foo/bar/baz')
+ expect(get('/gitlab/gitlabhq/refs/feature@45/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45', path: 'foo/bar/baz')
+ expect(get('/gitlab/gitlabhq/refs/stable/logs_tree/files.scss')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'stable', path: 'files.scss')
+ end
+ end
+
+ # diffs_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/diffs(.:format) projects/merge_requests#diffs
+ # commits_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/commits(.:format) projects/merge_requests#commits
+ # merge_namespace_project_merge_request POST /:namespace_id/:project_id/merge_requests/:id/merge(.:format) projects/merge_requests#merge
+ # merge_check_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/merge_check(.:format) projects/merge_requests#merge_check
+ # ci_status_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/ci_status(.:format) projects/merge_requests#ci_status
+ # toggle_subscription_namespace_project_merge_request POST /:namespace_id/:project_id/merge_requests/:id/toggle_subscription(.:format) projects/merge_requests#toggle_subscription
+ # branch_from_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/branch_from(.:format) projects/merge_requests#branch_from
+ # branch_to_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/branch_to(.:format) projects/merge_requests#branch_to
+ # update_branches_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/update_branches(.:format) projects/merge_requests#update_branches
+ # namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests(.:format) projects/merge_requests#index
+ # POST /:namespace_id/:project_id/merge_requests(.:format) projects/merge_requests#create
+ # new_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/new(.:format) projects/merge_requests#new
+ # edit_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/edit(.:format) projects/merge_requests#edit
+ # namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#show
+ # PATCH /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#update
+ # PUT /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#update
+ describe Projects::MergeRequestsController, 'routing' do
+ it 'to #diffs' do
+ expect(get('/gitlab/gitlabhq/merge_requests/1/diffs')).to route_to('projects/merge_requests#diffs', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
+ end
+
+ it 'to #commits' do
+ expect(get('/gitlab/gitlabhq/merge_requests/1/commits')).to route_to('projects/merge_requests#commits', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
+ end
+
+ it 'to #merge' do
+ expect(post('/gitlab/gitlabhq/merge_requests/1/merge')).to route_to(
+ 'projects/merge_requests#merge',
+ namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1'
+ )
+ end
+
+ it 'to #merge_check' do
+ expect(get('/gitlab/gitlabhq/merge_requests/1/merge_check')).to route_to('projects/merge_requests#merge_check', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
+ end
+
+ it 'to #branch_from' do
+ expect(get('/gitlab/gitlabhq/merge_requests/branch_from')).to route_to('projects/merge_requests#branch_from', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ end
+
+ it 'to #branch_to' do
+ expect(get('/gitlab/gitlabhq/merge_requests/branch_to')).to route_to('projects/merge_requests#branch_to', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ end
+
+ it 'to #show' do
+ expect(get('/gitlab/gitlabhq/merge_requests/1.diff')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'diff')
+ expect(get('/gitlab/gitlabhq/merge_requests/1.patch')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'patch')
+ end
+
+ it_behaves_like 'RESTful project resources' do
+ let(:controller) { 'merge_requests' }
+ let(:actions) { [:index, :create, :new, :edit, :show, :update] }
+ end
+ end
+
+ # raw_project_snippet GET /:project_id/snippets/:id/raw(.:format) snippets#raw
+ # project_snippets GET /:project_id/snippets(.:format) snippets#index
+ # POST /:project_id/snippets(.:format) snippets#create
+ # new_project_snippet GET /:project_id/snippets/new(.:format) snippets#new
+ # edit_project_snippet GET /:project_id/snippets/:id/edit(.:format) snippets#edit
+ # project_snippet GET /:project_id/snippets/:id(.:format) snippets#show
+ # PUT /:project_id/snippets/:id(.:format) snippets#update
+ # DELETE /:project_id/snippets/:id(.:format) snippets#destroy
+ describe SnippetsController, 'routing' do
+ it 'to #raw' do
+ expect(get('/gitlab/gitlabhq/snippets/1/raw')).to route_to('projects/snippets#raw', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
+ end
+
+ it 'to #index' do
+ expect(get('/gitlab/gitlabhq/snippets')).to route_to('projects/snippets#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ end
+
+ it 'to #create' do
+ expect(post('/gitlab/gitlabhq/snippets')).to route_to('projects/snippets#create', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ end
+
+ it 'to #new' do
+ expect(get('/gitlab/gitlabhq/snippets/new')).to route_to('projects/snippets#new', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ end
+
+ it 'to #edit' do
+ expect(get('/gitlab/gitlabhq/snippets/1/edit')).to route_to('projects/snippets#edit', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
+ end
+
+ it 'to #show' do
+ expect(get('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
+ end
+
+ it 'to #update' do
+ expect(put('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#update', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
+ end
+
+ it 'to #destroy' do
+ expect(delete('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
+ end
+ end
+
+ # test_project_hook GET /:project_id/hooks/:id/test(.:format) hooks#test
+ # project_hooks GET /:project_id/hooks(.:format) hooks#index
+ # POST /:project_id/hooks(.:format) hooks#create
+ # project_hook DELETE /:project_id/hooks/:id(.:format) hooks#destroy
+ describe Projects::HooksController, 'routing' do
+ it 'to #test' do
+ expect(get('/gitlab/gitlabhq/hooks/1/test')).to route_to('projects/hooks#test', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
+ end
+
+ it_behaves_like 'RESTful project resources' do
+ let(:actions) { [:index, :create, :destroy] }
+ let(:controller) { 'hooks' }
+ end
+ end
+
+ # project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /\h{7,40}/, project_id: /[^\/]+/}
+ describe Projects::CommitController, 'routing' do
+ it 'to #show' do
+ expect(get('/gitlab/gitlabhq/commit/4246fbd')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd')
+ expect(get('/gitlab/gitlabhq/commit/4246fbd.diff')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd', format: 'diff')
+ expect(get('/gitlab/gitlabhq/commit/4246fbd.patch')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd', format: 'patch')
+ expect(get('/gitlab/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5')
+ end
+ end
+
+ # patch_project_commit GET /:project_id/commits/:id/patch(.:format) commits#patch
+ # project_commits GET /:project_id/commits(.:format) commits#index
+ # POST /:project_id/commits(.:format) commits#create
+ # project_commit GET /:project_id/commits/:id(.:format) commits#show
+ describe Projects::CommitsController, 'routing' do
+ it_behaves_like 'RESTful project resources' do
+ let(:actions) { [:show] }
+ let(:controller) { 'commits' }
+ end
+
+ it 'to #show' do
+ expect(get('/gitlab/gitlabhq/commits/master.atom')).to route_to('projects/commits#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master.atom')
+ end
+ end
+
+ # project_project_members GET /:project_id/project_members(.:format) project_members#index
+ # POST /:project_id/project_members(.:format) project_members#create
+ # PUT /:project_id/project_members/:id(.:format) project_members#update
+ # DELETE /:project_id/project_members/:id(.:format) project_members#destroy
+ describe Projects::ProjectMembersController, 'routing' do
+ it_behaves_like 'RESTful project resources' do
+ let(:actions) { [:index, :create, :update, :destroy] }
+ let(:controller) { 'project_members' }
+ end
+ end
+
+ # project_milestones GET /:project_id/milestones(.:format) milestones#index
+ # POST /:project_id/milestones(.:format) milestones#create
+ # new_project_milestone GET /:project_id/milestones/new(.:format) milestones#new
+ # edit_project_milestone GET /:project_id/milestones/:id/edit(.:format) milestones#edit
+ # project_milestone GET /:project_id/milestones/:id(.:format) milestones#show
+ # PUT /:project_id/milestones/:id(.:format) milestones#update
+ # DELETE /:project_id/milestones/:id(.:format) milestones#destroy
+ describe Projects::MilestonesController, 'routing' do
+ it_behaves_like 'RESTful project resources' do
+ let(:controller) { 'milestones' }
+ let(:actions) { [:index, :create, :new, :edit, :show, :update] }
+ end
+ end
+
+ # project_labels GET /:project_id/labels(.:format) labels#index
+ describe Projects::LabelsController, 'routing' do
+ it 'to #index' do
+ expect(get('/gitlab/gitlabhq/labels')).to route_to('projects/labels#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ end
+ end
+
+ # sort_project_issues POST /:project_id/issues/sort(.:format) issues#sort
+ # bulk_update_project_issues POST /:project_id/issues/bulk_update(.:format) issues#bulk_update
+ # search_project_issues GET /:project_id/issues/search(.:format) issues#search
+ # project_issues GET /:project_id/issues(.:format) issues#index
+ # POST /:project_id/issues(.:format) issues#create
+ # new_project_issue GET /:project_id/issues/new(.:format) issues#new
+ # edit_project_issue GET /:project_id/issues/:id/edit(.:format) issues#edit
+ # project_issue GET /:project_id/issues/:id(.:format) issues#show
+ # PUT /:project_id/issues/:id(.:format) issues#update
+ # DELETE /:project_id/issues/:id(.:format) issues#destroy
+ describe Projects::IssuesController, 'routing' do
+ it 'to #bulk_update' do
+ expect(post('/gitlab/gitlabhq/issues/bulk_update')).to route_to('projects/issues#bulk_update', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ end
+
+ it_behaves_like 'RESTful project resources' do
+ let(:controller) { 'issues' }
+ let(:actions) { [:index, :create, :new, :edit, :show, :update] }
+ end
+ end
+
+ # project_notes GET /:project_id/notes(.:format) notes#index
+ # POST /:project_id/notes(.:format) notes#create
+ # project_note DELETE /:project_id/notes/:id(.:format) notes#destroy
+ describe Projects::NotesController, 'routing' do
+ it_behaves_like 'RESTful project resources' do
+ let(:actions) { [:index, :create, :destroy] }
+ let(:controller) { 'notes' }
+ end
+ end
+
+ # project_blame GET /:project_id/blame/:id(.:format) blame#show {id: /.+/, project_id: /[^\/]+/}
+ describe Projects::BlameController, 'routing' do
+ it 'to #show' do
+ expect(get('/gitlab/gitlabhq/blame/master/app/models/project.rb')).to route_to('projects/blame#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb')
+ expect(get('/gitlab/gitlabhq/blame/master/files.scss')).to route_to('projects/blame#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss')
+ end
+ end
+
+ # project_blob GET /:project_id/blob/:id(.:format) blob#show {id: /.+/, project_id: /[^\/]+/}
+ describe Projects::BlobController, 'routing' do
+ it 'to #show' do
+ expect(get('/gitlab/gitlabhq/blob/master/app/models/project.rb')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb')
+ expect(get('/gitlab/gitlabhq/blob/master/app/models/compare.rb')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/compare.rb')
+ expect(get('/gitlab/gitlabhq/blob/master/app/models/diff.js')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/diff.js')
+ expect(get('/gitlab/gitlabhq/blob/master/files.scss')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss')
+ end
+ end
+
+ # project_tree GET /:project_id/tree/:id(.:format) tree#show {id: /.+/, project_id: /[^\/]+/}
+ describe Projects::TreeController, 'routing' do
+ it 'to #show' do
+ expect(get('/gitlab/gitlabhq/tree/master/app/models/project.rb')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb')
+ expect(get('/gitlab/gitlabhq/tree/master/files.scss')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss')
+ end
+ end
+
+ # project_find_file GET /:namespace_id/:project_id/find_file/*id(.:format) projects/find_file#show {:id=>/.+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/html/}
+ # project_files GET /:namespace_id/:project_id/files/*id(.:format) projects/find_file#list {:id=>/(?:[^.]|\.(?!json$))+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/json/}
+ describe Projects::FindFileController, 'routing' do
+ it 'to #show' do
+ expect(get('/gitlab/gitlabhq/find_file/master')).to route_to('projects/find_file#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master')
+ end
+
+ it 'to #list' do
+ expect(get('/gitlab/gitlabhq/files/master.json')).to route_to('projects/find_file#list', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
+ end
+ end
+
+ describe Projects::BlobController, 'routing' do
+ it 'to #edit' do
+ expect(get('/gitlab/gitlabhq/edit/master/app/models/project.rb')).to(
+ route_to('projects/blob#edit',
+ namespace_id: 'gitlab', project_id: 'gitlabhq',
+ id: 'master/app/models/project.rb'))
+ end
+
+ it 'to #preview' do
+ expect(post('/gitlab/gitlabhq/preview/master/app/models/project.rb')).to(
+ route_to('projects/blob#preview',
+ namespace_id: 'gitlab', project_id: 'gitlabhq',
+ id: 'master/app/models/project.rb'))
+ end
+ end
+
+ # project_compare_index GET /:project_id/compare(.:format) compare#index {id: /[^\/]+/, project_id: /[^\/]+/}
+ # POST /:project_id/compare(.:format) compare#create {id: /[^\/]+/, project_id: /[^\/]+/}
+ # project_compare /:project_id/compare/:from...:to(.:format) compare#show {from: /.+/, to: /.+/, id: /[^\/]+/, project_id: /[^\/]+/}
+ describe Projects::CompareController, 'routing' do
+ it 'to #index' do
+ expect(get('/gitlab/gitlabhq/compare')).to route_to('projects/compare#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ end
+
+ it 'to #compare' do
+ expect(post('/gitlab/gitlabhq/compare')).to route_to('projects/compare#create', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ end
+
+ it 'to #show' do
+ expect(get('/gitlab/gitlabhq/compare/master...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'master', to: 'stable')
+ expect(get('/gitlab/gitlabhq/compare/issue/1234...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'issue/1234', to: 'stable')
+ end
+ end
+
+ describe Projects::NetworkController, 'routing' do
+ it 'to #show' do
+ expect(get('/gitlab/gitlabhq/network/master')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master')
+ expect(get('/gitlab/gitlabhq/network/ends-with.json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json')
+ expect(get('/gitlab/gitlabhq/network/master?format=json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
+ end
+ end
+
+ describe Projects::GraphsController, 'routing' do
+ it 'to #show' do
+ expect(get('/gitlab/gitlabhq/graphs/master')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master')
+ expect(get('/gitlab/gitlabhq/graphs/ends-with.json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json')
+ expect(get('/gitlab/gitlabhq/graphs/master?format=json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
+ end
+ end
+
+ describe Projects::ForksController, 'routing' do
+ it 'to #new' do
+ expect(get('/gitlab/gitlabhq/forks/new')).to route_to('projects/forks#new', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ end
+
+ it 'to #create' do
+ expect(post('/gitlab/gitlabhq/forks')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ end
+ end
+
+ # project_avatar DELETE /project/avatar(.:format) projects/avatars#destroy
+ describe Projects::AvatarsController, 'routing' do
+ it 'to #destroy' do
+ expect(delete('/gitlab/gitlabhq/avatar')).to route_to(
+ 'projects/avatars#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ end
end
end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index c18a2d55e43..9f6defe1450 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -9,7 +9,7 @@ require 'spec_helper'
# user_calendar_activities GET /u/:username/calendar_activities(.:format)
describe UsersController, "routing" do
it "to #show" do
- allow(User).to receive(:find_by).and_return(true)
+ allow_any_instance_of(UserUrlConstrainer).to receive(:matches?).and_return(true)
expect(get("/User")).to route_to('users#show', username: 'User')
end
@@ -195,6 +195,8 @@ describe Profiles::KeysController, "routing" do
# get all the ssh-keys of a user
it "to #get_keys" do
+ allow_any_instance_of(UserUrlConstrainer).to receive(:matches?).and_return(true)
+
expect(get("/foo.keys")).to route_to('profiles/keys#get_keys', username: 'foo')
end
end
@@ -261,20 +263,36 @@ describe "Authentication", "routing" do
end
describe "Groups", "routing" do
+ let(:name) { 'complex.group-namegit' }
+
+ before { allow_any_instance_of(GroupUrlConstrainer).to receive(:matches?).and_return(true) }
+
it "to #show" do
- expect(get("/groups/1")).to route_to('groups#show', id: '1')
+ expect(get("/groups/#{name}")).to route_to('groups#show', id: name)
+ end
+
+ it "also supports nested groups" do
+ expect(get("/#{name}/#{name}")).to route_to('groups#show', id: "#{name}/#{name}")
end
it "also display group#show on the short path" do
- allow(Group).to receive(:find_by_path).and_return(true)
+ expect(get("/#{name}")).to route_to('groups#show', id: name)
+ end
- expect(get('/1')).to route_to('groups#show', id: '1')
+ it "to #activity" do
+ expect(get("/groups/#{name}/activity")).to route_to('groups#activity', id: name)
end
- it "also display group#show with dot in the path" do
- allow(Group).to receive(:find_by_path).and_return(true)
+ it "to #issues" do
+ expect(get("/groups/#{name}/issues")).to route_to('groups#issues', id: name)
+ end
+
+ it "to #members" do
+ expect(get("/groups/#{name}/group_members")).to route_to('groups/group_members#index', group_id: name)
+ end
- expect(get('/group.with.dot')).to route_to('groups#show', id: 'group.with.dot')
+ it "also display group#show with slash in the path" do
+ expect(get('/group/subgroup')).to route_to('groups#show', id: 'group/subgroup')
end
end
diff --git a/spec/serializers/analytics_build_entity_spec.rb b/spec/serializers/analytics_build_entity_spec.rb
new file mode 100644
index 00000000000..86e703a6448
--- /dev/null
+++ b/spec/serializers/analytics_build_entity_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe AnalyticsBuildEntity do
+ let(:entity) do
+ described_class.new(build, request: double)
+ end
+
+ context 'build with an author' do
+ let(:user) { create(:user) }
+ let(:started_at) { 2.hours.ago }
+ let(:finished_at) { 1.hour.ago }
+ let(:build) { create(:ci_build, author: user, started_at: started_at, finished_at: finished_at) }
+
+ subject { entity.as_json }
+
+ before do
+ Timecop.freeze
+ end
+
+ after do
+ Timecop.return
+ end
+
+ it 'contains the URL' do
+ expect(subject).to include(:url)
+ end
+
+ it 'contains the author' do
+ expect(subject).to include(:author)
+ end
+
+ it 'does not contain sensitive information' do
+ expect(subject).not_to include(/token/)
+ expect(subject).not_to include(/variables/)
+ end
+
+ it 'contains the right started at' do
+ expect(subject[:date]).to eq('about 2 hours ago')
+ end
+
+ it 'contains the duration' do
+ expect(subject[:total_time]).to eq(hours: 1 )
+ end
+
+ context 'no started at or finished at date' do
+ let(:started_at) { nil }
+ let(:finished_at) { nil }
+
+ it 'does not blow up' do
+ expect{ subject[:date] }.not_to raise_error
+ end
+
+ it 'shows the right message' do
+ expect(subject[:date]).to eq('Not started')
+ end
+
+ it 'shows the right total time' do
+ expect(subject[:total_time]).to eq({})
+ end
+ end
+
+ context 'no started at date' do
+ let(:started_at) { nil }
+
+ it 'does not blow up' do
+ expect{ subject[:date] }.not_to raise_error
+ end
+
+ it 'shows the right message' do
+ expect(subject[:date]).to eq('Not started')
+ end
+
+ it 'shows the right total time' do
+ expect(subject[:total_time]).to eq({})
+ end
+ end
+
+ context 'no finished at date' do
+ let(:finished_at) { nil }
+
+ it 'does not blow up' do
+ expect{ subject[:date] }.not_to raise_error
+ end
+
+ it 'shows the right message' do
+ expect(subject[:date]).to eq('about 2 hours ago')
+ end
+
+ it 'shows the right total time' do
+ expect(subject[:total_time]).to eq({ hours: 2 })
+ end
+ end
+ end
+end
diff --git a/spec/serializers/analytics_build_serializer_spec.rb b/spec/serializers/analytics_build_serializer_spec.rb
new file mode 100644
index 00000000000..f0551c78671
--- /dev/null
+++ b/spec/serializers/analytics_build_serializer_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe AnalyticsBuildSerializer do
+ let(:serializer) do
+ described_class
+ .new.represent(resource)
+ end
+
+ let(:json) { serializer.as_json }
+ let(:resource) { create(:ci_build) }
+
+ context 'when there is a single object provided' do
+ it 'contains important elements of analyticsBuild' do
+ expect(json)
+ .to include(:name, :branch, :short_sha, :date, :total_time, :url, :author)
+ end
+ end
+end
diff --git a/spec/serializers/analytics_generic_entity_spec.rb b/spec/serializers/analytics_generic_entity_spec.rb
new file mode 100644
index 00000000000..68086216ba9
--- /dev/null
+++ b/spec/serializers/analytics_generic_entity_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe AnalyticsIssueEntity do
+ let(:user) { create(:user) }
+ let(:entity_hash) do
+ {
+ total_time: "172802.724419",
+ title: "Eos voluptatem inventore in sed.",
+ iid: "1",
+ id: "1",
+ created_at: "2016-11-12 15:04:02.948604",
+ author: user,
+ }
+ end
+
+ let(:project) { create(:empty_project) }
+ let(:request) { EntityRequest.new(project: project, entity: :merge_request) }
+
+ let(:entity) do
+ described_class.new(entity_hash, request: request, project: project)
+ end
+
+ context 'generic entity' do
+ subject { entity.as_json }
+
+ it 'contains the entity URL' do
+ expect(subject).to include(:url)
+ end
+
+ it 'contains the author' do
+ expect(subject).to include(:author)
+ end
+
+ it 'does not contain sensitive information' do
+ expect(subject).not_to include(/token/)
+ expect(subject).not_to include(/variables/)
+ end
+ end
+end
diff --git a/spec/serializers/analytics_issue_serializer_spec.rb b/spec/serializers/analytics_issue_serializer_spec.rb
new file mode 100644
index 00000000000..6afbb2df35c
--- /dev/null
+++ b/spec/serializers/analytics_issue_serializer_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe AnalyticsIssueSerializer do
+ let(:serializer) do
+ described_class
+ .new(project: project, entity: :merge_request)
+ .represent(resource)
+ end
+
+ let(:user) { create(:user) }
+ let(:json) { serializer.as_json }
+ let(:project) { create(:project) }
+ let(:resource) do
+ {
+ total_time: "172802.724419",
+ title: "Eos voluptatem inventore in sed.",
+ iid: "1",
+ id: "1",
+ created_at: "2016-11-12 15:04:02.948604",
+ author: user,
+ }
+ end
+
+ context 'when there is a single object provided' do
+ it 'contains important elements of the issue' do
+ expect(json).to include(:title, :iid, :created_at, :total_time, :url, :author)
+ end
+ end
+end
diff --git a/spec/serializers/analytics_merge_request_serializer_spec.rb b/spec/serializers/analytics_merge_request_serializer_spec.rb
new file mode 100644
index 00000000000..cdfae27193f
--- /dev/null
+++ b/spec/serializers/analytics_merge_request_serializer_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe AnalyticsMergeRequestSerializer do
+ let(:serializer) do
+ described_class
+ .new(project: project, entity: :merge_request)
+ .represent(resource)
+ end
+
+ let(:user) { create(:user) }
+ let(:json) { serializer.as_json }
+ let(:project) { create(:project) }
+ let(:resource) do
+ {
+ total_time: "172802.724419",
+ title: "Eos voluptatem inventore in sed.",
+ iid: "1",
+ id: "1",
+ state: 'open',
+ created_at: "2016-11-12 15:04:02.948604",
+ author: user
+ }
+ end
+
+ context 'when there is a single object provided' do
+ it 'contains important elements of the merge request' do
+ expect(json).to include(:title, :iid, :created_at, :total_time, :url, :author, :state)
+ end
+ end
+end
diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/build_entity_spec.rb
index 2734f5bedca..60c9642ee2c 100644
--- a/spec/serializers/build_entity_spec.rb
+++ b/spec/serializers/build_entity_spec.rb
@@ -1,31 +1,38 @@
require 'spec_helper'
describe BuildEntity do
+ let(:build) { create(:ci_build) }
+
let(:entity) do
described_class.new(build, request: double)
end
subject { entity.as_json }
- context 'when build is a regular job' do
- let(:build) { create(:ci_build) }
+ it 'contains paths to build page and retry action' do
+ expect(subject).to include(:build_path, :retry_path)
+ end
- it 'contains url to build page and retry action' do
- expect(subject).to include(:build_url, :retry_url)
- expect(subject).not_to include(:play_url)
- end
+ it 'does not contain sensitive information' do
+ expect(subject).not_to include(/token/)
+ expect(subject).not_to include(/variables/)
+ end
+
+ it 'contains timestamps' do
+ expect(subject).to include(:created_at, :updated_at)
+ end
- it 'does not contain sensitive information' do
- expect(subject).not_to include(/token/)
- expect(subject).not_to include(/variables/)
+ context 'when build is a regular job' do
+ it 'does not contain path to play action' do
+ expect(subject).not_to include(:play_path)
end
end
context 'when build is a manual action' do
let(:build) { create(:ci_build, :manual) }
- it 'contains url to play action' do
- expect(subject).to include(:play_url)
+ it 'contains path to play action' do
+ expect(subject).to include(:play_path)
end
end
end
diff --git a/spec/serializers/commit_entity_spec.rb b/spec/serializers/commit_entity_spec.rb
index 628e35c9a28..15f11ac3df9 100644
--- a/spec/serializers/commit_entity_spec.rb
+++ b/spec/serializers/commit_entity_spec.rb
@@ -31,7 +31,11 @@ describe CommitEntity do
end
end
- it 'contains commit URL' do
+ it 'contains path to commit' do
+ expect(subject).to include(:commit_path)
+ end
+
+ it 'contains URL to commit' do
expect(subject).to include(:commit_url)
end
diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb
index 51b6de91571..ea87771e2a2 100644
--- a/spec/serializers/deployment_entity_spec.rb
+++ b/spec/serializers/deployment_entity_spec.rb
@@ -15,6 +15,6 @@ describe DeploymentEntity do
it 'exposes nested information about branch' do
expect(subject[:ref][:name]).to eq 'master'
- expect(subject[:ref][:ref_url]).not_to be_empty
+ expect(subject[:ref][:ref_path]).not_to be_empty
end
end
diff --git a/spec/serializers/entity_date_helper_spec.rb b/spec/serializers/entity_date_helper_spec.rb
new file mode 100644
index 00000000000..b9cc2f64831
--- /dev/null
+++ b/spec/serializers/entity_date_helper_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe EntityDateHelper do
+ let(:date_helper_class) { Class.new { include EntityDateHelper }.new }
+
+ it 'converts 0 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(0)).to eq(seconds: 0)
+ end
+
+ it 'converts 40 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(40)).to eq(seconds: 40)
+ end
+
+ it 'converts 60 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(60)).to eq(mins: 1)
+ end
+
+ it 'converts 70 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(70)).to eq(mins: 1, seconds: 10)
+ end
+
+ it 'converts 3600 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(3600)).to eq(hours: 1)
+ end
+
+ it 'converts 3750 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(3750)).to eq(hours: 1, mins: 2, seconds: 30)
+ end
+
+ it 'converts 86400 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(86400)).to eq(days: 1)
+ end
+
+ it 'converts 86560 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(86560)).to eq(days: 1, mins: 2, seconds: 40)
+ end
+
+ it 'converts 86760 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(99760)).to eq(days: 1, hours: 3, mins: 42, seconds: 40)
+ end
+
+ it 'converts 986760 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(986760)).to eq(days: 11, hours: 10, mins: 6)
+ end
+end
diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb
index 4ca8c299147..57728ce3181 100644
--- a/spec/serializers/environment_entity_spec.rb
+++ b/spec/serializers/environment_entity_spec.rb
@@ -13,6 +13,6 @@ describe EnvironmentEntity do
end
it 'exposes core elements of environment' do
- expect(subject).to include(:id, :name, :state, :environment_url)
+ expect(subject).to include(:id, :name, :state, :environment_path)
end
end
diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb
index 37bc086826c..b7ed4eb0239 100644
--- a/spec/serializers/environment_serializer_spec.rb
+++ b/spec/serializers/environment_serializer_spec.rb
@@ -27,13 +27,9 @@ describe EnvironmentSerializer do
let(:deployable) { create(:ci_build) }
let(:resource) { deployment.environment }
- it 'it generates payload for single object' do
- expect(json).to be_an_instance_of Hash
- end
-
it 'contains important elements of environment' do
expect(json)
- .to include(:name, :external_url, :environment_url, :last_deployment)
+ .to include(:name, :external_url, :environment_path, :last_deployment)
end
it 'contains relevant information about last deployment' do
diff --git a/spec/services/access_token_validation_service_spec.rb b/spec/services/access_token_validation_service_spec.rb
new file mode 100644
index 00000000000..87f093ee8ce
--- /dev/null
+++ b/spec/services/access_token_validation_service_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe AccessTokenValidationService, services: true do
+ describe ".include_any_scope?" do
+ it "returns true if the required scope is present in the token's scopes" do
+ token = double("token", scopes: [:api, :read_user])
+
+ expect(described_class.new(token).include_any_scope?([:api])).to be(true)
+ end
+
+ it "returns true if more than one of the required scopes is present in the token's scopes" do
+ token = double("token", scopes: [:api, :read_user, :other_scope])
+
+ expect(described_class.new(token).include_any_scope?([:api, :other_scope])).to be(true)
+ end
+
+ it "returns true if the list of required scopes is an exact match for the token's scopes" do
+ token = double("token", scopes: [:api, :read_user, :other_scope])
+
+ expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true)
+ end
+
+ it "returns true if the list of required scopes contains all of the token's scopes, in addition to others" do
+ token = double("token", scopes: [:api, :read_user])
+
+ expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true)
+ end
+
+ it 'returns true if the list of required scopes is blank' do
+ token = double("token", scopes: [])
+
+ expect(described_class.new(token).include_any_scope?([])).to be(true)
+ end
+
+ it "returns false if there are no scopes in common between the required scopes and the token scopes" do
+ token = double("token", scopes: [:api, :read_user])
+
+ expect(described_class.new(token).include_any_scope?([:other_scope])).to be(false)
+ end
+ end
+end
diff --git a/spec/services/after_branch_delete_service_spec.rb b/spec/services/after_branch_delete_service_spec.rb
new file mode 100644
index 00000000000..d29e0addb53
--- /dev/null
+++ b/spec/services/after_branch_delete_service_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe AfterBranchDeleteService, services: true do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:service) { described_class.new(project, user) }
+
+ describe '#execute' do
+ it 'stops environments attached to branch' do
+ expect(service).to receive(:stop_environments)
+
+ service.execute('feature')
+ end
+ end
+end
diff --git a/spec/services/chat_names/authorize_user_service_spec.rb b/spec/services/chat_names/authorize_user_service_spec.rb
new file mode 100644
index 00000000000..d50bfb0492c
--- /dev/null
+++ b/spec/services/chat_names/authorize_user_service_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe ChatNames::AuthorizeUserService, services: true do
+ describe '#execute' do
+ let(:service) { create(:service) }
+
+ subject { described_class.new(service, params).execute }
+
+ context 'when all parameters are valid' do
+ let(:params) { { team_id: 'T0001', team_domain: 'myteam', user_id: 'U0001', user_name: 'user' } }
+
+ it 'requests a new token' do
+ is_expected.to be_url
+ end
+ end
+
+ context 'when there are missing parameters' do
+ let(:params) { {} }
+
+ it 'does not request a new token' do
+ is_expected.to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/services/chat_names/find_user_service_spec.rb b/spec/services/chat_names/find_user_service_spec.rb
new file mode 100644
index 00000000000..51441e8f3be
--- /dev/null
+++ b/spec/services/chat_names/find_user_service_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe ChatNames::FindUserService, services: true do
+ describe '#execute' do
+ let(:service) { create(:service) }
+
+ subject { described_class.new(service, params).execute }
+
+ context 'find user mapping' do
+ let(:user) { create(:user) }
+ let!(:chat_name) { create(:chat_name, user: user, service: service) }
+
+ context 'when existing user is requested' do
+ let(:params) { { team_id: chat_name.team_id, user_id: chat_name.chat_id } }
+
+ it 'returns the existing user' do
+ is_expected.to eq(user)
+ end
+
+ it 'updates when last time chat name was used' do
+ subject
+
+ expect(chat_name.reload.last_used_at).to be_like_time(Time.now)
+ end
+ end
+
+ context 'when different user is requested' do
+ let(:params) { { team_id: chat_name.team_id, user_id: 'non-existing-user' } }
+
+ it 'returns existing user' do
+ is_expected.to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 4aadd009f3e..ceaca96e25b 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -210,5 +210,22 @@ describe Ci::CreatePipelineService, services: true do
expect(result.manual_actions).not_to be_empty
end
end
+
+ context 'with environment' do
+ before do
+ config = YAML.dump(deploy: { environment: { name: "review/$CI_BUILD_REF_NAME" }, script: 'ls' })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'creates the environment' do
+ result = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: [{ message: 'some msg' }])
+
+ expect(result).to be_persisted
+ expect(Environment.find_by(name: "review/master")).not_to be_nil
+ end
+ end
end
end
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index ff113efd916..ebb11166964 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -1,31 +1,10 @@
require 'spec_helper'
describe Ci::ProcessPipelineService, services: true do
- let(:pipeline) { create(:ci_pipeline, ref: 'master') }
+ let(:pipeline) { create(:ci_empty_pipeline, ref: 'master') }
let(:user) { create(:user) }
- let(:config) { nil }
-
- before do
- allow(pipeline).to receive(:ci_yaml_file).and_return(config)
- end
describe '#execute' do
- def all_builds
- pipeline.builds
- end
-
- def builds
- all_builds.where.not(status: [:created, :skipped])
- end
-
- def process_pipeline
- described_class.new(pipeline.project, user).execute(pipeline)
- end
-
- def succeed_pending
- builds.pending.update_all(status: 'success')
- end
-
context 'start queuing next builds' do
before do
create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage_idx: 0)
@@ -223,10 +202,6 @@ describe Ci::ProcessPipelineService, services: true do
pipeline.builds.running_or_pending.each(&:success)
expect(manual_actions).to be_many # production and clear cache
end
-
- def manual_actions
- pipeline.manual_actions
- end
end
end
@@ -282,15 +257,6 @@ describe Ci::ProcessPipelineService, services: true do
expect(builds.map(&:status)).to eq(%w[success skipped pending])
end
end
-
- def create_build(name, stage_idx, when_value = nil)
- create(:ci_build,
- :created,
- pipeline: pipeline,
- name: name,
- stage_idx: stage_idx,
- when: when_value)
- end
end
context 'when failed build in the middle stage is retried' do
@@ -327,65 +293,92 @@ describe Ci::ProcessPipelineService, services: true do
end
end
- context 'creates a builds from .gitlab-ci.yml' do
- let(:config) do
- YAML.dump({
- rspec: {
- stage: 'test',
- script: 'rspec'
- },
- rubocop: {
- stage: 'test',
- script: 'rubocop'
- },
- deploy: {
- stage: 'deploy',
- script: 'deploy'
- }
- })
+ context 'when there are builds that are not created yet' do
+ let(:pipeline) do
+ create(:ci_pipeline, config: config)
end
- # Using stubbed .gitlab-ci.yml created in commit factory
- #
+ let(:config) do
+ { rspec: { stage: 'test', script: 'rspec' },
+ deploy: { stage: 'deploy', script: 'rsync' } }
+ end
before do
- stub_ci_pipeline_yaml_file(config)
create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage: 'build', stage_idx: 0)
create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage: 'build', stage_idx: 0)
end
- it 'when processing a pipeline' do
- # Currently we have two builds with state created
+ it 'processes the pipeline' do
+ # Currently we have five builds with state created
+ #
expect(builds.count).to eq(0)
expect(all_builds.count).to eq(2)
- # Create builds will mark the created as pending
- expect(process_pipeline).to be_truthy
+ # Process builds service will enqueue builds from the first stage.
+ #
+ process_pipeline
+
expect(builds.count).to eq(2)
expect(all_builds.count).to eq(2)
- # When we builds succeed we will create a rest of pipeline from .gitlab-ci.yml
- # We will have 2 succeeded, 2 pending (from stage test), total 5 (one more build from deploy)
+ # When builds succeed we will enqueue remaining builds.
+ #
+ # We will have 2 succeeded, 1 pending (from stage test), total 4 (two
+ # additional build from `.gitlab-ci.yml`).
+ #
succeed_pending
- expect(process_pipeline).to be_truthy
+ process_pipeline
+
expect(builds.success.count).to eq(2)
- expect(builds.pending.count).to eq(2)
- expect(all_builds.count).to eq(5)
+ expect(builds.pending.count).to eq(1)
+ expect(all_builds.count).to eq(4)
- # When we succeed the 2 pending from stage test,
- # We will queue a deploy stage, no new builds will be created
+ # When pending build succeeds in stage test, we enqueue deploy stage.
+ #
succeed_pending
- expect(process_pipeline).to be_truthy
+ process_pipeline
+
expect(builds.pending.count).to eq(1)
- expect(builds.success.count).to eq(4)
- expect(all_builds.count).to eq(5)
+ expect(builds.success.count).to eq(3)
+ expect(all_builds.count).to eq(4)
- # When we succeed last pending build, we will have a total of 5 succeeded builds, no new builds will be created
+ # When the last one succeeds we have 4 successful builds.
+ #
succeed_pending
- expect(process_pipeline).to be_falsey
- expect(builds.success.count).to eq(5)
- expect(all_builds.count).to eq(5)
+ process_pipeline
+
+ expect(builds.success.count).to eq(4)
+ expect(all_builds.count).to eq(4)
end
end
end
+
+ def all_builds
+ pipeline.builds
+ end
+
+ def builds
+ all_builds.where.not(status: [:created, :skipped])
+ end
+
+ def process_pipeline
+ described_class.new(pipeline.project, user).execute(pipeline)
+ end
+
+ def succeed_pending
+ builds.pending.update_all(status: 'success')
+ end
+
+ def manual_actions
+ pipeline.manual_actions
+ end
+
+ def create_build(name, stage_idx, when_value = nil)
+ create(:ci_build,
+ :created,
+ pipeline: pipeline,
+ name: name,
+ stage_idx: stage_idx,
+ when: when_value)
+ end
end
diff --git a/spec/services/ci/send_pipeline_notification_service_spec.rb b/spec/services/ci/send_pipeline_notification_service_spec.rb
deleted file mode 100644
index 288302cc94f..00000000000
--- a/spec/services/ci/send_pipeline_notification_service_spec.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-require 'spec_helper'
-
-describe Ci::SendPipelineNotificationService, services: true do
- let(:pipeline) do
- create(:ci_pipeline,
- project: project,
- sha: project.commit('master').sha,
- user: user,
- status: status)
- end
-
- let(:project) { create(:project) }
- let(:user) { create(:user) }
-
- subject{ described_class.new(pipeline) }
-
- describe '#execute' do
- before do
- reset_delivered_emails!
- end
-
- shared_examples 'sending emails' do
- it 'sends an email to pipeline user' do
- perform_enqueued_jobs do
- subject.execute([user.email])
- end
-
- email = ActionMailer::Base.deliveries.last
- expect(email.subject).to include(email_subject)
- expect(email.to).to eq([user.email])
- end
- end
-
- context 'with success pipeline' do
- let(:status) { 'success' }
- let(:email_subject) { "Pipeline ##{pipeline.id} has succeeded" }
-
- it_behaves_like 'sending emails'
- end
-
- context 'with failed pipeline' do
- let(:status) { 'failed' }
- let(:email_subject) { "Pipeline ##{pipeline.id} has failed" }
-
- it_behaves_like 'sending emails'
- end
- end
-end
diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb
new file mode 100644
index 00000000000..6f7d1a5d28d
--- /dev/null
+++ b/spec/services/ci/stop_environments_service_spec.rb
@@ -0,0 +1,105 @@
+require 'spec_helper'
+
+describe Ci::StopEnvironmentsService, services: true do
+ let(:project) { create(:project, :private) }
+ let(:user) { create(:user) }
+
+ let(:service) { described_class.new(project, user) }
+
+ describe '#execute' do
+ context 'when environment with review app exists' do
+ before do
+ create(:environment, :with_review_app, project: project,
+ ref: 'feature')
+ end
+
+ context 'when user has permission to stop environment' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'when environment is associated with removed branch' do
+ it 'stops environment' do
+ expect_environment_stopped_on('feature')
+ end
+ end
+
+ context 'when environment is associated with different branch' do
+ it 'does not stop environment' do
+ expect_environment_not_stopped_on('master')
+ end
+ end
+
+ context 'when specified branch does not exist' do
+ it 'does not stop environment' do
+ expect_environment_not_stopped_on('non/existent/branch')
+ end
+ end
+
+ context 'when no branch not specified' do
+ it 'does not stop environment' do
+ expect_environment_not_stopped_on(nil)
+ end
+ end
+
+ context 'when environment is not stoppable' do
+ before do
+ allow_any_instance_of(Environment)
+ .to receive(:stoppable?).and_return(false)
+ end
+
+ it 'does not stop environment' do
+ expect_environment_not_stopped_on('feature')
+ end
+ end
+ end
+
+ context 'when user does not have permission to stop environment' do
+ before do
+ project.team << [user, :guest]
+ end
+
+ it 'does not stop environment' do
+ expect_environment_not_stopped_on('master')
+ end
+ end
+ end
+
+ context 'when there is no environment associated with review app' do
+ before do
+ create(:environment, project: project)
+ end
+
+ context 'when user has permission to stop environments' do
+ before do
+ project.team << [user, :master]
+ end
+
+ it 'does not stop environment' do
+ expect_environment_not_stopped_on('master')
+ end
+ end
+ end
+
+ context 'when environment does not exist' do
+ it 'does not raise error' do
+ expect { service.execute('master') }
+ .not_to raise_error
+ end
+ end
+ end
+
+ def expect_environment_stopped_on(branch)
+ expect_any_instance_of(Environment)
+ .to receive(:stop!)
+
+ service.execute(branch)
+ end
+
+ def expect_environment_not_stopped_on(branch)
+ expect_any_instance_of(Environment)
+ .not_to receive(:stop!)
+
+ service.execute(branch)
+ end
+end
diff --git a/spec/services/delete_branch_service_spec.rb b/spec/services/delete_branch_service_spec.rb
new file mode 100644
index 00000000000..336f5dafb5b
--- /dev/null
+++ b/spec/services/delete_branch_service_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe DeleteBranchService, services: true do
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+ let(:user) { create(:user) }
+ let(:service) { described_class.new(project, user) }
+
+ describe '#execute' do
+ context 'when user has access to push to repository' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'removes the branch' do
+ expect(branch_exists?('feature')).to be true
+
+ result = service.execute('feature')
+
+ expect(result[:status]).to eq :success
+ expect(branch_exists?('feature')).to be false
+ end
+ end
+
+ context 'when user does not have access to push to repository' do
+ it 'does not remove branch' do
+ expect(branch_exists?('feature')).to be true
+
+ result = service.execute('feature')
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq 'You dont have push access to repo'
+ expect(branch_exists?('feature')).to be true
+ end
+ end
+ end
+
+ def branch_exists?(branch_name)
+ repository.ref_exists?("refs/heads/#{branch_name}")
+ end
+end
diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/delete_merged_branches_service_spec.rb
new file mode 100644
index 00000000000..181488e89c7
--- /dev/null
+++ b/spec/services/delete_merged_branches_service_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe DeleteMergedBranchesService, services: true do
+ subject(:service) { described_class.new(project, project.owner) }
+
+ let(:project) { create(:project) }
+
+ context '#execute' do
+ context 'unprotected branches' do
+ before do
+ service.execute
+ end
+
+ it 'deletes a branch that was merged' do
+ expect(project.repository.branch_names).not_to include('improve/awesome')
+ end
+
+ it 'keeps branch that is unmerged' do
+ expect(project.repository.branch_names).to include('feature')
+ end
+
+ it 'keeps "master"' do
+ expect(project.repository.branch_names).to include('master')
+ end
+ end
+
+ context 'protected branches' do
+ before do
+ create(:protected_branch, name: 'improve/awesome', project: project)
+ service.execute
+ end
+
+ it 'keeps protected branch' do
+ expect(project.repository.branch_names).to include('improve/awesome')
+ end
+ end
+
+ context 'user without rights' do
+ let(:user) { create(:user) }
+
+ it 'cannot execute' do
+ expect { described_class.new(project, user).execute }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+ end
+
+ context '#async_execute' do
+ it 'calls DeleteMergedBranchesWorker async' do
+ expect(DeleteMergedBranchesWorker).to receive(:perform_async)
+
+ service.async_execute
+ end
+ end
+end
diff --git a/spec/services/destroy_group_service_spec.rb b/spec/services/destroy_group_service_spec.rb
index da724643604..538e85cdc89 100644
--- a/spec/services/destroy_group_service_spec.rb
+++ b/spec/services/destroy_group_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe DestroyGroupService, services: true do
+ include DatabaseConnectionHelpers
+
let!(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:project) { create(:project, namespace: group) }
@@ -50,6 +52,44 @@ describe DestroyGroupService, services: true do
describe 'asynchronous delete' do
it_behaves_like 'group destruction', true
+
+ context 'potential race conditions' do
+ context "when the `GroupDestroyWorker` task runs immediately" do
+ it "deletes the group" do
+ # Commit the contents of this spec's transaction so far
+ # so subsequent db connections can see it.
+ #
+ # DO NOT REMOVE THIS LINE, even if you see a WARNING with "No
+ # transaction is currently in progress". Without this, this
+ # spec will always be green, since the group created in setup
+ # cannot be seen by any other connections / threads in this spec.
+ Group.connection.commit_db_transaction
+
+ group_record = run_with_new_database_connection do |conn|
+ conn.execute("SELECT * FROM namespaces WHERE id = #{group.id}").first
+ end
+
+ expect(group_record).not_to be_nil
+
+ # Execute the contents of `GroupDestroyWorker` in a separate thread, to
+ # simulate data manipulation by the Sidekiq worker (different database
+ # connection / transaction).
+ expect(GroupDestroyWorker).to receive(:perform_async).and_wrap_original do |m, group_id, user_id|
+ Thread.new { m[group_id, user_id] }.join(5)
+ end
+
+ # Kick off the initial group destroy in a new thread, so that
+ # it doesn't share this spec's database transaction.
+ Thread.new { DestroyGroupService.new(group, user).async_execute }.join(5)
+
+ group_record = run_with_new_database_connection do |conn|
+ conn.execute("SELECT * FROM namespaces WHERE id = #{group.id}").first
+ end
+
+ expect(group_record).to be_nil
+ end
+ end
+ end
end
describe 'synchronous delete' do
diff --git a/spec/services/discussions/resolve_service_spec.rb b/spec/services/discussions/resolve_service_spec.rb
new file mode 100644
index 00000000000..12c3cdf28c6
--- /dev/null
+++ b/spec/services/discussions/resolve_service_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Discussions::ResolveService do
+ describe '#execute' do
+ let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first }
+ let(:project) { merge_request.project }
+ let(:merge_request) { discussion.noteable }
+ let(:user) { create(:user) }
+ let(:service) { described_class.new(discussion.noteable.project, user, merge_request: merge_request) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ it "doesn't resolve discussions the user can't resolve" do
+ expect(discussion).to receive(:can_resolve?).with(user).and_return(false)
+
+ service.execute(discussion)
+
+ expect(discussion.resolved?).to be(false)
+ end
+
+ it 'resolves the discussion' do
+ service.execute(discussion)
+
+ expect(discussion.resolved?).to be(true)
+ end
+
+ it 'executes the notification service' do
+ expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService).to receive(:execute).with(discussion.noteable)
+
+ service.execute(discussion)
+ end
+
+ it 'adds a system note to the discussion' do
+ issue = create(:issue, project: project)
+
+ expect(SystemNoteService).to receive(:discussion_continued_in_issue).with(discussion, project, user, issue)
+ service = described_class.new(project, user, merge_request: merge_request, follow_up_issue: issue)
+ service.execute(discussion)
+ end
+
+ it 'can resolve multiple discussions at once' do
+ other_discussion = Discussion.for_diff_notes([create(:diff_note_on_merge_request, noteable: discussion.noteable, project: discussion.noteable.source_project)]).first
+
+ service.execute([discussion, other_discussion])
+
+ expect(discussion.resolved?).to be(true)
+ expect(other_discussion.resolved?).to be(true)
+ end
+ end
+end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index cea7e6429f9..e7624e70725 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -27,27 +27,14 @@ describe GitPushService, services: true do
it { is_expected.to be_truthy }
- it 'flushes general cached data' do
- expect(project.repository).to receive(:expire_cache).
- with('master', newrev)
+ it 'calls the after_push_commit hook' do
+ expect(project.repository).to receive(:after_push_commit).with('master')
subject
end
- it 'flushes the visible content cache' do
- expect(project.repository).to receive(:expire_has_visible_content_cache)
-
- subject
- end
-
- it 'flushes the branches cache' do
- expect(project.repository).to receive(:expire_branches_cache)
-
- subject
- end
-
- it 'flushes the branch count cache' do
- expect(project.repository).to receive(:expire_branch_count_cache)
+ it 'calls the after_create_branch hook' do
+ expect(project.repository).to receive(:after_create_branch)
subject
end
@@ -56,21 +43,8 @@ describe GitPushService, services: true do
context 'existing branch' do
it { is_expected.to be_truthy }
- it 'flushes general cached data' do
- expect(project.repository).to receive(:expire_cache).
- with('master', newrev)
-
- subject
- end
-
- it 'does not flush the branches cache' do
- expect(project.repository).not_to receive(:expire_branches_cache)
-
- subject
- end
-
- it 'does not flush the branch count cache' do
- expect(project.repository).not_to receive(:expire_branch_count_cache)
+ it 'calls the after_push_commit hook' do
+ expect(project.repository).to receive(:after_push_commit).with('master')
subject
end
@@ -81,27 +55,14 @@ describe GitPushService, services: true do
it { is_expected.to be_truthy }
- it 'flushes the visible content cache' do
- expect(project.repository).to receive(:expire_has_visible_content_cache)
-
- subject
- end
-
- it 'flushes the branches cache' do
- expect(project.repository).to receive(:expire_branches_cache)
-
- subject
- end
-
- it 'flushes the branch count cache' do
- expect(project.repository).to receive(:expire_branch_count_cache)
+ it 'calls the after_push_commit hook' do
+ expect(project.repository).to receive(:after_push_commit).with('master')
subject
end
- it 'flushes general cached data' do
- expect(project.repository).to receive(:expire_cache).
- with('master', newrev)
+ it 'calls the after_remove_branch hook' do
+ expect(project.repository).to receive(:after_remove_branch)
subject
end
@@ -302,7 +263,7 @@ describe GitPushService, services: true do
author_email: commit_author.email
)
- allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit).
+ allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit).
and_return(commit)
allow(project.repository).to receive(:commits_between).and_return([commit])
@@ -360,7 +321,7 @@ describe GitPushService, services: true do
committed_date: commit_time
)
- allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit).
+ allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit).
and_return(commit)
allow(project.repository).to receive(:commits_between).and_return([commit])
@@ -399,7 +360,7 @@ describe GitPushService, services: true do
allow(project.repository).to receive(:commits_between).
and_return([closing_commit])
- allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit).
+ allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit).
and_return(closing_commit)
project.team << [commit_author, :master]
@@ -490,7 +451,17 @@ describe GitPushService, services: true do
context "closing an issue" do
let(:message) { "this is some work.\n\ncloses JIRA-1" }
- let(:comment_body) { { body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json }
+ let(:comment_body) { { body: "Issue solved with [#{closing_commit.id}|http://#{Gitlab.config.gitlab.host}/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json }
+
+ before do
+ open_issue = JIRA::Resource::Issue.new(jira_tracker.client, attrs: { "id" => "JIRA-1" })
+ closed_issue = open_issue.dup
+ allow(open_issue).to receive(:resolution).and_return(false)
+ allow(closed_issue).to receive(:resolution).and_return(true)
+ allow(JIRA::Resource::Issue).to receive(:find).and_return(open_issue, closed_issue)
+
+ allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return("JIRA-1")
+ end
context "using right markdown" do
it "initiates one api call to jira server to close the issue" do
@@ -588,6 +559,51 @@ describe GitPushService, services: true do
end
end
+ describe '#update_caches' do
+ let(:service) do
+ described_class.new(project,
+ user,
+ oldrev: sample_commit.parent_id,
+ newrev: sample_commit.id,
+ ref: 'refs/heads/master')
+ end
+
+ context 'on the default branch' do
+ before do
+ allow(service).to receive(:is_default_branch?).and_return(true)
+ end
+
+ it 'flushes the caches of any special files that have been changed' do
+ commit = double(:commit)
+ diff = double(:diff, new_path: 'README.md')
+
+ expect(commit).to receive(:raw_diffs).with(deltas_only: true).
+ and_return([diff])
+
+ service.push_commits = [commit]
+
+ expect(ProjectCacheWorker).to receive(:perform_async).
+ with(project.id, %i(readme))
+
+ service.update_caches
+ end
+ end
+
+ context 'on a non-default branch' do
+ before do
+ allow(service).to receive(:is_default_branch?).and_return(false)
+ end
+
+ it 'does not flush any conditional caches' do
+ expect(ProjectCacheWorker).to receive(:perform_async).
+ with(project.id, []).
+ and_call_original
+
+ service.update_caches
+ end
+ end
+ end
+
def execute_service(project, user, oldrev, newrev, ref)
service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref )
service.execute
diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb
index 0879e3ab4c8..bd074b9bd71 100644
--- a/spec/services/git_tag_push_service_spec.rb
+++ b/spec/services/git_tag_push_service_spec.rb
@@ -18,7 +18,7 @@ describe GitTagPushService, services: true do
end
it 'flushes general cached data' do
- expect(project.repository).to receive(:expire_cache)
+ expect(project.repository).to receive(:before_push_tag)
subject
end
@@ -28,12 +28,6 @@ describe GitTagPushService, services: true do
subject
end
-
- it 'flushes the tag count cache' do
- expect(project.repository).to receive(:expire_tag_count_cache)
-
- subject
- end
end
describe "Git Tag Push Data" do
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index 6f7ce8ca992..5f3020b6525 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -260,14 +260,14 @@ describe Issuable::BulkUpdateService, services: true do
it 'subscribes the given user' do
bulk_update(issues, subscription_event: 'subscribe')
- expect(issues).to all(be_subscribed(user))
+ expect(issues).to all(be_subscribed(user, project))
end
end
describe 'unsubscribe from issues' do
let(:issues) do
create_list(:closed_issue, 2, project: project) do |issue|
- issue.subscriptions.create(user: user, subscribed: true)
+ issue.subscriptions.create(user: user, project: project, subscribed: true)
end
end
@@ -275,7 +275,7 @@ describe Issuable::BulkUpdateService, services: true do
bulk_update(issues, subscription_event: 'unsubscribe')
issues.each do |issue|
- expect(issue).not_to be_subscribed(user)
+ expect(issue).not_to be_subscribed(user, project)
end
end
end
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
new file mode 100644
index 00000000000..4cfba35c830
--- /dev/null
+++ b/spec/services/issues/build_service_spec.rb
@@ -0,0 +1,130 @@
+require 'spec_helper.rb'
+
+describe Issues::BuildService, services: true do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'for discussions in a merge request' do
+ let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project) }
+ let(:issue) { described_class.new(project, user, merge_request_for_resolving_discussions: merge_request).execute }
+
+ def position_on_line(line_number)
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: line_number,
+ diff_refs: merge_request.diff_refs
+ )
+ end
+
+ describe '#items_for_discussions' do
+ it 'has an item for each discussion' do
+ create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.source_project, position: position_on_line(13))
+ service = described_class.new(project, user, merge_request_for_resolving_discussions: merge_request)
+
+ service.execute
+
+ expect(service.items_for_discussions.size).to eq(2)
+ end
+ end
+
+ describe '#item_for_discussion' do
+ let(:service) { described_class.new(project, user, merge_request_for_resolving_discussions: merge_request) }
+
+ it 'mentions the author of the note' do
+ discussion = Discussion.new([create(:diff_note_on_merge_request, author: create(:user, username: 'author'))])
+ expect(service.item_for_discussion(discussion)).to include('@author')
+ end
+
+ it 'wraps the note in a blockquote' do
+ note_text = "This is a string\n"\
+ ">>>\n"\
+ "with a blockquote\n"\
+ "> That has a quote\n"\
+ ">>>\n"
+ note_result = "This is a string\n"\
+ "> with a blockquote\n"\
+ "> > That has a quote\n"
+ discussion = Discussion.new([create(:diff_note_on_merge_request, note: note_text)])
+ expect(service.item_for_discussion(discussion)).to include(">>>\n#{note_result}\n>>>")
+ end
+ end
+
+ describe '#execute' do
+ it 'has the merge request reference in the title' do
+ expect(issue.title).to include(merge_request.title)
+ end
+
+ it 'has the reference of the merge request in the description' do
+ expect(issue.description).to include(merge_request.to_reference)
+ end
+
+ it 'does not assign title when a title was given' do
+ issue = described_class.new(project, user,
+ merge_request_for_resolving_discussions: merge_request,
+ title: 'What an issue').execute
+
+ expect(issue.title).to eq('What an issue')
+ end
+
+ it 'does not assign description when a description was given' do
+ issue = described_class.new(project, user,
+ merge_request_for_resolving_discussions: merge_request,
+ description: 'Fix at your earliest conveignance').execute
+
+ expect(issue.description).to eq('Fix at your earliest conveignance')
+ end
+
+ describe 'with multiple discussions' do
+ before do
+ create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.target_project, position: position_on_line(15))
+ end
+
+ it 'mentions all the authors in the description' do
+ authors = merge_request.diff_discussions.map(&:author)
+
+ expect(issue.description).to include(*authors.map(&:to_reference))
+ end
+
+ it 'has a link for each unresolved discussion in the description' do
+ notes = merge_request.diff_discussions.map(&:first_note)
+ links = notes.map { |note| Gitlab::UrlBuilder.build(note) }
+
+ expect(issue.description).to include(*links)
+ end
+
+ it 'mentions additional notes' do
+ create_list(:diff_note_on_merge_request, 2, noteable: merge_request, project: merge_request.target_project, position: position_on_line(15))
+
+ expect(issue.description).to include('(+2 comments)')
+ end
+ end
+ end
+ end
+
+ context 'For a merge request without discussions' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ describe '#execute' do
+ it 'mentions the merge request in the description' do
+ issue = described_class.new(project, user, merge_request_for_resolving_discussions: merge_request).execute
+
+ expect(issue.description).to include("Review the conversation in #{merge_request.to_reference}")
+ end
+ end
+ end
+
+ describe '#execute' do
+ it 'builds a new issues with given params' do
+ issue = described_class.new(project, user, title: 'Issue #1', description: 'Issue description').execute
+
+ expect(issue.title).to eq('Issue #1')
+ expect(issue.description).to eq('Issue description')
+ end
+ end
+end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 4465f22a001..7a54373963e 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -62,7 +62,7 @@ describe Issues::CloseService, services: true do
it 'creates system note about issue reassign' do
note = issue.notes.last
- expect(note.note).to include "Status changed to closed"
+ expect(note.note).to include "closed"
end
it 'marks todos as done' do
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 5c0331ebe66..8bde61ee336 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -136,5 +136,48 @@ describe Issues::CreateService, services: true do
end
it_behaves_like 'new issuable record that supports slash commands'
+
+ context 'for 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 }
+ let(:opts) { { merge_request_for_resolving_discussions: merge_request } }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ it 'resolves the discussion for the merge request' do
+ described_class.new(project, user, opts).execute
+ discussion.first_note.reload
+
+ expect(discussion.resolved?).to be(true)
+ end
+
+ it 'added a system note to the discussion' do
+ described_class.new(project, user, opts).execute
+
+ reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first
+
+ expect(reloaded_discussion.last_note.system).to eq(true)
+ end
+
+ it 'assigns the title and description for the issue' do
+ issue = described_class.new(project, user, opts).execute
+
+ expect(issue.title).not_to be_nil
+ expect(issue.description).not_to be_nil
+ end
+
+ it 'can set nil explicityly to the title and description' do
+ issue = described_class.new(project, user,
+ merge_request_for_resolving_discussions: merge_request,
+ description: nil,
+ title: nil).execute
+
+ expect(issue.description).to be_nil
+ expect(issue.title).to be_nil
+ end
+ end
end
end
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index f0ded06b785..db196ed5751 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -81,11 +81,11 @@ describe Issues::MoveService, services: true do
end
it 'adds system note to old issue at the end' do
- expect(old_issue.notes.last.note).to match /^Moved to/
+ expect(old_issue.notes.last.note).to start_with 'moved to'
end
it 'adds system note to new issue at the end' do
- expect(new_issue.notes.last.note).to match /^Moved from/
+ expect(new_issue.notes.last.note).to start_with 'moved from'
end
it 'closes old issue' do
@@ -151,7 +151,7 @@ describe Issues::MoveService, services: true do
end
it 'adds a system note about move after rewritten notes' do
- expect(system_notes.last.note).to match /^Moved from/
+ expect(system_notes.last.note).to match /^moved from/
end
it 'preserves orignal author of comment' do
@@ -189,7 +189,7 @@ describe Issues::MoveService, services: true do
it 'rewrites references using a cross reference to old project' do
expect(new_note.note)
- .to eq "Note with reference to merge request #{old_project.to_reference}!1"
+ .to eq "Note with reference to merge request #{old_project.to_reference(new_project)}!1"
end
end
@@ -217,7 +217,7 @@ describe Issues::MoveService, services: true do
it 'rewrites referenced issues creating cross project reference' do
expect(new_issue.description)
- .to eq "Some description #{old_project.to_reference}#{another_issue.to_reference}"
+ .to eq "Some description #{another_issue.to_reference(new_project)}"
end
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 1638a46ed51..eafbea46905 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -2,6 +2,8 @@
require 'spec_helper'
describe Issues::UpdateService, services: true do
+ include EmailHelpers
+
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
@@ -91,24 +93,24 @@ describe Issues::UpdateService, services: true do
end
it 'creates system note about issue reassign' do
- note = find_note('Reassigned to')
+ note = find_note('assigned to')
expect(note).not_to be_nil
- expect(note.note).to include "Reassigned to \@#{user2.username}"
+ expect(note.note).to include "assigned to #{user2.to_reference}"
end
it 'creates system note about issue label edit' do
- note = find_note('Added ~')
+ note = find_note('added ~')
expect(note).not_to be_nil
- expect(note.note).to include "Added ~#{label.id} label"
+ expect(note.note).to include "added #{label.to_reference} label"
end
it 'creates system note about title change' do
- note = find_note('Changed title:')
+ note = find_note('changed title')
expect(note).not_to be_nil
- expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**'
+ expect(note.note).to eq 'changed title from **{-Old-} title** to **{+New+} title**'
end
end
end
@@ -128,10 +130,10 @@ describe Issues::UpdateService, services: true do
it 'creates system note about confidentiality change' do
update_issue(confidential: true)
- note = find_note('Made the issue confidential')
+ note = find_note('made the issue confidential')
expect(note).not_to be_nil
- expect(note.note).to eq 'Made the issue confidential'
+ expect(note.note).to eq 'made the issue confidential'
end
it 'executes confidential issue hooks' do
@@ -215,7 +217,7 @@ describe Issues::UpdateService, services: true do
let!(:subscriber) do
create(:user).tap do |u|
- label.toggle_subscription(u)
+ label.toggle_subscription(u, project)
project.team << [u, :developer]
end
end
@@ -269,8 +271,8 @@ describe Issues::UpdateService, services: true do
before { update_issue(description: "- [x] Task 1\n- [X] Task 2") }
it 'creates system note about task status change' do
- note1 = find_note('Marked the task **Task 1** as completed')
- note2 = find_note('Marked the task **Task 2** as completed')
+ note1 = find_note('marked the task **Task 1** as completed')
+ note2 = find_note('marked the task **Task 2** as completed')
expect(note1).not_to be_nil
expect(note2).not_to be_nil
@@ -284,8 +286,8 @@ describe Issues::UpdateService, services: true do
end
it 'creates system note about task status change' do
- note1 = find_note('Marked the task **Task 1** as incomplete')
- note2 = find_note('Marked the task **Task 2** as incomplete')
+ note1 = find_note('marked the task **Task 1** as incomplete')
+ note2 = find_note('marked the task **Task 2** as incomplete')
expect(note1).not_to be_nil
expect(note2).not_to be_nil
@@ -299,7 +301,7 @@ describe Issues::UpdateService, services: true do
end
it 'does not create a system note' do
- note = find_note('Marked the task **Task 2** as incomplete')
+ note = find_note('marked the task **Task 2** as incomplete')
expect(note).to be_nil
end
@@ -312,7 +314,7 @@ describe Issues::UpdateService, services: true do
end
it 'does not create a system note referencing the position the old item' do
- note = find_note('Marked the task **Two** as incomplete')
+ note = find_note('marked the task **Two** as incomplete')
expect(note).to be_nil
end
@@ -374,5 +376,10 @@ describe Issues::UpdateService, services: true do
let(:mentionable) { issue }
include_examples 'updating mentions', Issues::UpdateService
end
+
+ include_examples 'issuable update service' do
+ let(:open_issuable) { issue }
+ let(:closed_issuable) { create(:closed_issue, project: project) }
+ end
end
end
diff --git a/spec/services/labels/transfer_service_spec.rb b/spec/services/labels/transfer_service_spec.rb
index ddf3527dc0f..13654a0881c 100644
--- a/spec/services/labels/transfer_service_spec.rb
+++ b/spec/services/labels/transfer_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Labels::TransferService, services: true do
describe '#execute' do
- let(:user) { create(:user) }
+ let(:user) { create(:admin) }
let(:group_1) { create(:group) }
let(:group_2) { create(:group) }
let(:group_3) { create(:group) }
diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb
index 7b090343a3e..7d5a66801db 100644
--- a/spec/services/members/approve_access_request_service_spec.rb
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
describe Members::ApproveAccessRequestService, services: true do
let(:user) { create(:user) }
let(:access_requester) { create(:user) }
- let(:project) { create(:project, :public) }
- let(:group) { create(:group, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
+ let(:group) { create(:group, :public, :access_requestable) }
let(:opts) { {} }
shared_examples 'a service raising ActiveRecord::RecordNotFound' do
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index 9995f3488af..574df6e0f42 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -26,6 +26,7 @@ describe Members::DestroyService, services: true do
context 'when the given member is an access requester' do
before do
source.members.find_by(user_id: member_user).destroy
+ source.update_attributes(request_access_enabled: true)
source.request_access(member_user)
end
let(:access_requester) { source.requesters.find_by(user_id: member_user) }
diff --git a/spec/services/members/request_access_service_spec.rb b/spec/services/members/request_access_service_spec.rb
index 0d2d5f03199..853c125dadb 100644
--- a/spec/services/members/request_access_service_spec.rb
+++ b/spec/services/members/request_access_service_spec.rb
@@ -2,8 +2,6 @@ require 'spec_helper'
describe Members::RequestAccessService, services: true do
let(:user) { create(:user) }
- let(:project) { create(:project, :private) }
- let(:group) { create(:group, :private) }
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
it 'raises Gitlab::Access::AccessDeniedError' do
@@ -31,27 +29,26 @@ describe Members::RequestAccessService, services: true do
end
context 'when current user cannot request access to the project' do
- it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:source) { project }
+ %i[project group].each do |source_type|
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { create(source_type, :private) }
+ end
end
+ end
- it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:source) { group }
+ context 'when access requests are disabled' do
+ %i[project group].each do |source_type|
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { create(source_type, :public) }
+ end
end
end
context 'when current user can request access to the project' do
- before do
- project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- group.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- end
-
- it_behaves_like 'a service creating a access request' do
- let(:source) { project }
- end
-
- it_behaves_like 'a service creating a access request' do
- let(:source) { group }
+ %i[project group].each do |source_type|
+ it_behaves_like 'a service creating a access request' do
+ let(:source) { create(source_type, :public, :access_requestable) }
+ end
end
end
end
diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
index dd656c3bbb7..bb7830c7eea 100644
--- a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
+++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
@@ -1,29 +1,46 @@
require 'spec_helper'
-# Write specs in this file.
describe MergeRequests::AddTodoWhenBuildFailsService do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { create(:project) }
let(:sha) { '1234567890abcdef1234567890abcdef12345678' }
- let(:pipeline) { create(:ci_pipeline_with_one_job, ref: merge_request.source_branch, project: project, sha: sha) }
- let(:service) { MergeRequests::AddTodoWhenBuildFailsService.new(project, user, commit_message: 'Awesome message') }
+ let(:ref) { merge_request.source_branch }
+
+ let(:pipeline) do
+ create(:ci_pipeline_with_one_job, ref: ref,
+ project: project,
+ sha: sha)
+ end
+
+ let(:service) do
+ described_class.new(project, user, commit_message: 'Awesome message')
+ end
+
let(:todo_service) { TodoService.new }
let(:merge_request) do
- create(:merge_request, merge_user: user, source_branch: 'master',
- target_branch: 'feature', source_project: project, target_project: project,
+ create(:merge_request, merge_user: user,
+ source_branch: 'master',
+ target_branch: 'feature',
+ source_project: project,
+ target_project: project,
state: 'opened')
end
before do
- allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
+ allow_any_instance_of(MergeRequest)
+ .to receive(:head_pipeline)
+ .and_return(pipeline)
+
allow(service).to receive(:todo_service).and_return(todo_service)
end
describe '#execute' do
context 'commit status with ref' do
- let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, pipeline: pipeline) }
+ let(:commit_status) do
+ create(:generic_commit_status, ref: ref, pipeline: pipeline)
+ end
it 'notifies the todo service' do
expect(todo_service).to receive(:merge_request_build_failed).with(merge_request)
@@ -32,7 +49,7 @@ describe MergeRequests::AddTodoWhenBuildFailsService do
end
context 'commit status with non-HEAD ref' do
- let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch) }
+ let(:commit_status) { create(:generic_commit_status, ref: ref) }
it 'does not notify the todo service' do
expect(todo_service).not_to receive(:merge_request_build_failed)
@@ -48,6 +65,18 @@ describe MergeRequests::AddTodoWhenBuildFailsService do
service.execute(commit_status)
end
end
+
+ context 'when commit status is a build allowed to fail' do
+ let(:commit_status) do
+ create(:ci_build, :allowed_to_fail, ref: ref, pipeline: pipeline)
+ end
+
+ it 'does not create todo' do
+ expect(todo_service).not_to receive(:merge_request_build_failed)
+
+ service.execute(commit_status)
+ end
+ end
end
describe '#close' do
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 3f5df049ea2..dc945ca4868 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -24,6 +24,8 @@ describe MergeRequests::BuildService, services: true do
end
before do
+ project.team << [user, :guest]
+
allow(CompareService).to receive_message_chain(:new, :execute).and_return(compare)
allow(project).to receive(:commit).and_return(commit_1)
allow(project).to receive(:commit).and_return(commit_2)
@@ -168,6 +170,16 @@ describe MergeRequests::BuildService, services: true do
expect(merge_request.title).to eq("Resolve \"#{issue.title}\"")
end
+ context 'when issue is not accessible to user' do
+ before do
+ project.team.truncate
+ end
+
+ it 'uses branch title as the merge request title' do
+ expect(merge_request.title).to eq("#{issue.iid} fix issue")
+ end
+ end
+
context 'issue does not exist' do
let(:source_branch) { "#{issue.iid.succ}-fix-issue" }
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index 24c25e4350f..5f6a7716beb 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -42,7 +42,7 @@ describe MergeRequests::CloseService, services: true do
it 'creates system note about merge_request reassign' do
note = @merge_request.notes.last
- expect(note.note).to include 'Status changed to closed'
+ expect(note.note).to include 'closed'
end
it 'marks todos as done' do
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
index 3a71776e81f..08829e4be70 100644
--- a/spec/services/merge_requests/get_urls_service_spec.rb
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -4,8 +4,8 @@ describe MergeRequests::GetUrlsService do
let(:project) { create(:project, :public) }
let(:service) { MergeRequests::GetUrlsService.new(project) }
let(:source_branch) { "my_branch" }
- let(:new_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" }
- let(:show_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" }
+ let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" }
+ let(:show_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" }
let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" }
let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
@@ -115,7 +115,7 @@ describe MergeRequests::GetUrlsService do
let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" }
let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/existing_branch" }
let(:changes) { "#{new_branch_changes}\n#{existing_branch_changes}" }
- let(:new_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" }
+ let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" }
it 'returns 2 urls for both creating new and showing merge request' do
result = service.execute(changes)
diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
index 807f89e80b7..05cdbe5287a 100644
--- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
+++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
@@ -10,6 +10,7 @@ describe MergeRequests::MergeRequestDiffCacheService do
expect(Rails.cache).to receive(:read).with(cache_key).and_return({})
expect(Rails.cache).to receive(:write).with(cache_key, anything)
+ allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(double("text?" => true))
subject.execute(merge_request)
end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index f93d7732a9a..5a89acc96a4 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -34,7 +34,7 @@ describe MergeRequests::MergeService, services: true do
it 'creates system note about merge_request merge' do
note = merge_request.notes.last
- expect(note.note).to include 'Status changed to merged'
+ expect(note.note).to include 'merged'
end
end
@@ -59,14 +59,19 @@ describe MergeRequests::MergeService, services: true do
include JiraServiceHelper
let(:jira_tracker) { project.create_jira_service }
+ let(:jira_issue) { ExternalIssue.new('JIRA-123', project) }
+ let(:commit) { double('commit', safe_message: "Fixes #{jira_issue.to_reference}") }
before do
project.update_attributes!(has_external_issue_tracker: true)
jira_service_settings
+ stub_jira_urls(jira_issue.id)
+ allow(merge_request).to receive(:commits).and_return([commit])
end
it 'closes issues on JIRA issue tracker' do
jira_issue = ExternalIssue.new('JIRA-123', project)
+ stub_jira_urls(jira_issue)
commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit])
@@ -75,9 +80,22 @@ describe MergeRequests::MergeService, services: true do
service.execute(merge_request)
end
+ context "when jira_issue_transition_id is not present" do
+ before { allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(nil) }
+
+ it "does not close issue" do
+ allow(jira_tracker).to receive_messages(jira_issue_transition_id: nil)
+
+ expect_any_instance_of(JiraService).not_to receive(:transition_issue)
+
+ service.execute(merge_request)
+ end
+ end
+
context "wrong issue markdown" do
it 'does not close issues on JIRA issue tracker' do
- jira_issue = ExternalIssue.new('#123', project)
+ jira_issue = ExternalIssue.new('#JIRA-123', project)
+ stub_jira_urls(jira_issue)
commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit])
diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
deleted file mode 100644
index 1f90efdbd6a..00000000000
--- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
+++ /dev/null
@@ -1,163 +0,0 @@
-require 'spec_helper'
-
-describe MergeRequests::MergeWhenBuildSucceedsService do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
-
- let(:mr_merge_if_green_enabled) do
- create(:merge_request, merge_when_build_succeeds: true, merge_user: user,
- source_branch: "master", target_branch: 'feature',
- source_project: project, target_project: project, state: "opened")
- end
-
- let(:pipeline) { create(:ci_pipeline_with_one_job, ref: mr_merge_if_green_enabled.source_branch, project: project) }
- let(:service) { MergeRequests::MergeWhenBuildSucceedsService.new(project, user, commit_message: 'Awesome message') }
-
- describe "#execute" do
- let(:merge_request) do
- create(:merge_request, target_project: project, source_project: project,
- source_branch: "feature", target_branch: 'master')
- end
-
- context 'first time enabling' do
- before do
- allow(merge_request).to receive(:pipeline).and_return(pipeline)
- service.execute(merge_request)
- end
-
- it 'sets the params, merge_user, and flag' do
- expect(merge_request).to be_valid
- expect(merge_request.merge_when_build_succeeds).to be_truthy
- expect(merge_request.merge_params).to eq commit_message: 'Awesome message'
- expect(merge_request.merge_user).to be user
- end
-
- it 'creates a system note' do
- note = merge_request.notes.last
- expect(note.note).to match /Enabled an automatic merge when the build for (\w+\/\w+@)?[0-9a-z]{8}/
- end
- end
-
- context 'already approved' do
- let(:service) { MergeRequests::MergeWhenBuildSucceedsService.new(project, user, new_key: true) }
- let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) }
-
- before do
- allow(mr_merge_if_green_enabled).to receive(:pipeline).and_return(pipeline)
- allow(mr_merge_if_green_enabled).to receive(:mergeable?).and_return(true)
- allow(pipeline).to receive(:success?).and_return(true)
- end
-
- it 'updates the merge params' do
- expect(SystemNoteService).not_to receive(:merge_when_build_succeeds)
-
- service.execute(mr_merge_if_green_enabled)
- expect(mr_merge_if_green_enabled.merge_params).to have_key(:new_key)
- end
- end
- end
-
- describe "#trigger" do
- let(:merge_request_ref) { mr_merge_if_green_enabled.source_branch }
- let(:merge_request_head) do
- project.commit(mr_merge_if_green_enabled.source_branch).id
- end
-
- context 'when triggered by pipeline with valid ref and sha' do
- let(:triggering_pipeline) do
- create(:ci_pipeline, project: project, ref: merge_request_ref,
- sha: merge_request_head, status: 'success')
- end
-
- it "merges all merge requests with merge when build succeeds enabled" do
- expect(MergeWorker).to receive(:perform_async)
- service.trigger(triggering_pipeline)
- end
- end
-
- context 'when triggered by an old pipeline' do
- let(:old_pipeline) do
- create(:ci_pipeline, project: project, ref: merge_request_ref,
- sha: '1234abcdef', status: 'success')
- end
-
- it 'it does not merge merge request' do
- expect(MergeWorker).not_to receive(:perform_async)
- service.trigger(old_pipeline)
- end
- end
-
- context 'when triggered by pipeline from a different branch' do
- let(:unrelated_pipeline) do
- create(:ci_pipeline, project: project, ref: 'feature',
- sha: merge_request_head, status: 'success')
- end
-
- it 'does not merge request' do
- expect(MergeWorker).not_to receive(:perform_async)
- service.trigger(unrelated_pipeline)
- end
- end
- end
-
- describe "#cancel" do
- before do
- service.cancel(mr_merge_if_green_enabled)
- end
-
- it "resets all the merge_when_build_succeeds params" do
- expect(mr_merge_if_green_enabled.merge_when_build_succeeds).to be_falsey
- expect(mr_merge_if_green_enabled.merge_params).to eq({})
- expect(mr_merge_if_green_enabled.merge_user).to be nil
- end
-
- it 'Posts a system note' do
- note = mr_merge_if_green_enabled.notes.last
- expect(note.note).to include 'Canceled the automatic merge'
- end
- end
-
- describe 'pipeline integration' do
- context 'when there are multiple stages in the pipeline' do
- let(:ref) { mr_merge_if_green_enabled.source_branch }
- let(:sha) { project.commit(ref).id }
-
- let(:pipeline) do
- create(:ci_empty_pipeline, ref: ref, sha: sha, project: project)
- end
-
- let!(:build) do
- create(:ci_build, :created, pipeline: pipeline, ref: ref,
- name: 'build', stage: 'build')
- end
-
- let!(:test) do
- create(:ci_build, :created, pipeline: pipeline, ref: ref,
- name: 'test', stage: 'test')
- end
-
- before do
- # This behavior of MergeRequest: we instantiate a new object
- allow_any_instance_of(MergeRequest).to receive(:pipeline).and_wrap_original do
- Ci::Pipeline.find(pipeline.id)
- end
- end
-
- it "doesn't merge if any of stages failed" do
- expect(MergeWorker).not_to receive(:perform_async)
-
- build.success
- test.reload
- test.drop
- end
-
- it 'merges when all stages succeeded' do
- expect(MergeWorker).to receive(:perform_async)
-
- build.success
- test.reload
- test.success
- end
- end
- end
-end
diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
new file mode 100644
index 00000000000..f92978a33a3
--- /dev/null
+++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
@@ -0,0 +1,179 @@
+require 'spec_helper'
+
+describe MergeRequests::MergeWhenPipelineSucceedsService do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:mr_merge_if_green_enabled) do
+ create(:merge_request, merge_when_build_succeeds: true, merge_user: user,
+ source_branch: "master", target_branch: 'feature',
+ source_project: project, target_project: project, state: "opened")
+ end
+
+ let(:pipeline) do
+ create(:ci_pipeline_with_one_job, ref: mr_merge_if_green_enabled.source_branch,
+ project: project)
+ end
+
+ let(:service) do
+ described_class.new(project, user, commit_message: 'Awesome message')
+ end
+
+ describe "#execute" do
+ let(:merge_request) do
+ create(:merge_request, target_project: project, source_project: project,
+ source_branch: "feature", target_branch: 'master')
+ end
+
+ context 'first time enabling' do
+ before do
+ allow(merge_request)
+ .to receive(:head_pipeline)
+ .and_return(pipeline)
+
+ service.execute(merge_request)
+ end
+
+ it 'sets the params, merge_user, and flag' do
+ expect(merge_request).to be_valid
+ expect(merge_request.merge_when_build_succeeds).to be_truthy
+ expect(merge_request.merge_params).to eq commit_message: 'Awesome message'
+ expect(merge_request.merge_user).to be user
+ end
+
+ it 'creates a system note' do
+ note = merge_request.notes.last
+ expect(note.note).to match /enabled an automatic merge when the pipeline for (\w+\/\w+@)?\h{8}/
+ end
+ end
+
+ context 'already approved' do
+ let(:service) { described_class.new(project, user, new_key: true) }
+ let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) }
+
+ before do
+ allow(mr_merge_if_green_enabled).to receive(:head_pipeline)
+ .and_return(pipeline)
+
+ allow(mr_merge_if_green_enabled).to receive(:mergeable?)
+ .and_return(true)
+
+ allow(pipeline).to receive(:success?).and_return(true)
+ end
+
+ it 'updates the merge params' do
+ expect(SystemNoteService).not_to receive(:merge_when_build_succeeds)
+
+ service.execute(mr_merge_if_green_enabled)
+ expect(mr_merge_if_green_enabled.merge_params).to have_key(:new_key)
+ end
+ end
+ end
+
+ describe "#trigger" do
+ let(:merge_request_ref) { mr_merge_if_green_enabled.source_branch }
+ let(:merge_request_head) do
+ project.commit(mr_merge_if_green_enabled.source_branch).id
+ end
+
+ context 'when triggered by pipeline with valid ref and sha' do
+ let(:triggering_pipeline) do
+ create(:ci_pipeline, project: project, ref: merge_request_ref,
+ sha: merge_request_head, status: 'success')
+ end
+
+ it "merges all merge requests with merge when build succeeds enabled" do
+ expect(MergeWorker).to receive(:perform_async)
+ service.trigger(triggering_pipeline)
+ end
+ end
+
+ context 'when triggered by an old pipeline' do
+ let(:old_pipeline) do
+ create(:ci_pipeline, project: project, ref: merge_request_ref,
+ sha: '1234abcdef', status: 'success')
+ end
+
+ it 'it does not merge merge request' do
+ expect(MergeWorker).not_to receive(:perform_async)
+ service.trigger(old_pipeline)
+ end
+ end
+
+ context 'when triggered by pipeline from a different branch' do
+ let(:unrelated_pipeline) do
+ create(:ci_pipeline, project: project, ref: 'feature',
+ sha: merge_request_head, status: 'success')
+ end
+
+ it 'does not merge request' do
+ expect(MergeWorker).not_to receive(:perform_async)
+ service.trigger(unrelated_pipeline)
+ end
+ end
+ end
+
+ describe "#cancel" do
+ before do
+ service.cancel(mr_merge_if_green_enabled)
+ end
+
+ it "resets all the merge_when_build_succeeds params" do
+ expect(mr_merge_if_green_enabled.merge_when_build_succeeds).to be_falsey
+ expect(mr_merge_if_green_enabled.merge_params).to eq({})
+ expect(mr_merge_if_green_enabled.merge_user).to be nil
+ end
+
+ it 'Posts a system note' do
+ note = mr_merge_if_green_enabled.notes.last
+ expect(note.note).to include 'canceled the automatic merge'
+ end
+ end
+
+ describe 'pipeline integration' do
+ context 'when there are multiple stages in the pipeline' do
+ let(:ref) { mr_merge_if_green_enabled.source_branch }
+ let(:sha) { project.commit(ref).id }
+
+ let(:pipeline) do
+ create(:ci_empty_pipeline, ref: ref, sha: sha, project: project)
+ end
+
+ let!(:build) do
+ create(:ci_build, :created, pipeline: pipeline, ref: ref,
+ name: 'build', stage: 'build')
+ end
+
+ let!(:test) do
+ create(:ci_build, :created, pipeline: pipeline, ref: ref,
+ name: 'test', stage: 'test')
+ end
+
+ before do
+ # This behavior of MergeRequest: we instantiate a new object
+ #
+ allow_any_instance_of(MergeRequest)
+ .to receive(:head_pipeline)
+ .and_wrap_original do
+ Ci::Pipeline.find(pipeline.id)
+ end
+ end
+
+ it "doesn't merge if any of stages failed" do
+ expect(MergeWorker).not_to receive(:perform_async)
+
+ build.success
+ test.reload
+ test.drop
+ end
+
+ it 'merges when all stages succeeded' do
+ expect(MergeWorker).to receive(:perform_async)
+
+ build.success
+ test.reload
+ test.success
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index e515bc9f89c..7e3705983fb 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -76,10 +76,10 @@ describe MergeRequests::RefreshService, services: true do
reload_mrs
end
- it { expect(@merge_request.notes.last.note).to include('changed to merged') }
+ it { expect(@merge_request.notes.last.note).to include('merged') }
it { expect(@merge_request).to be_merged }
it { expect(@fork_merge_request).to be_merged }
- it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') }
+ it { expect(@fork_merge_request.notes.last.note).to include('merged') }
it { expect(@build_failed_todo).to be_done }
it { expect(@fork_build_failed_todo).to be_done }
end
@@ -95,11 +95,11 @@ describe MergeRequests::RefreshService, services: true do
reload_mrs
end
- it { expect(@merge_request.notes.last.note).to include('changed to merged') }
+ it { expect(@merge_request.notes.last.note).to include('merged') }
it { expect(@merge_request).to be_merged }
it { expect(@merge_request.diffs.size).to be > 0 }
it { expect(@fork_merge_request).to be_merged }
- it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') }
+ it { expect(@fork_merge_request.notes.last.note).to include('merged') }
it { expect(@build_failed_todo).to be_done }
it { expect(@fork_build_failed_todo).to be_done }
end
@@ -119,24 +119,34 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request.notes).to be_empty }
it { expect(@merge_request).to be_open }
- it { expect(@fork_merge_request.notes.last.note).to include('Added 28 commits') }
+ it { expect(@fork_merge_request.notes.last.note).to include('added 28 commits') }
it { expect(@fork_merge_request).to be_open }
it { expect(@build_failed_todo).to be_pending }
it { expect(@fork_build_failed_todo).to be_pending }
end
context 'push to fork repo target branch' do
- before do
- service.new(@fork_project, @user).execute(@oldrev, @newrev, 'refs/heads/feature')
- reload_mrs
+ describe 'changes to merge requests' do
+ before do
+ service.new(@fork_project, @user).execute(@oldrev, @newrev, 'refs/heads/feature')
+ reload_mrs
+ end
+
+ it { expect(@merge_request.notes).to be_empty }
+ it { expect(@merge_request).to be_open }
+ it { expect(@fork_merge_request.notes).to be_empty }
+ it { expect(@fork_merge_request).to be_open }
+ it { expect(@build_failed_todo).to be_pending }
+ it { expect(@fork_build_failed_todo).to be_pending }
end
- it { expect(@merge_request.notes).to be_empty }
- it { expect(@merge_request).to be_open }
- it { expect(@fork_merge_request.notes).to be_empty }
- it { expect(@fork_merge_request).to be_open }
- it { expect(@build_failed_todo).to be_pending }
- it { expect(@fork_build_failed_todo).to be_pending }
+ describe 'merge request diff' do
+ it 'does not reload the diff of the merge request made from fork' do
+ expect do
+ service.new(@fork_project, @user).execute(@oldrev, @newrev, 'refs/heads/feature')
+ end.not_to change { @fork_merge_request.reload.merge_request_diff }
+ end
+ end
end
context 'push to origin repo target branch after fork project was removed' do
@@ -146,7 +156,7 @@ describe MergeRequests::RefreshService, services: true do
reload_mrs
end
- it { expect(@merge_request.notes.last.note).to include('changed to merged') }
+ it { expect(@merge_request.notes.last.note).to include('merged') }
it { expect(@merge_request).to be_merged }
it { expect(@fork_merge_request).to be_open }
it { expect(@fork_merge_request.notes).to be_empty }
@@ -169,8 +179,8 @@ describe MergeRequests::RefreshService, services: true do
expect(@merge_request).to be_open
notes = @fork_merge_request.notes.reorder(:created_at).map(&:note)
- expect(notes[0]).to include('Restored source branch `master`')
- expect(notes[1]).to include('Added 28 commits')
+ expect(notes[0]).to include('restored source branch `master`')
+ expect(notes[1]).to include('added 28 commits')
expect(@fork_merge_request).to be_open
end
end
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index af7424a76a9..a99d4eac9bd 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -41,7 +41,7 @@ describe MergeRequests::ReopenService, services: true do
it 'creates system note about merge_request reopen' do
note = merge_request.notes.last
- expect(note.note).to include 'Status changed to reopened'
+ expect(note.note).to include 'reopened'
end
end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 2433a7dad06..88c786947d3 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe MergeRequests::UpdateService, services: true do
+ include EmailHelpers
+
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
@@ -79,31 +81,31 @@ describe MergeRequests::UpdateService, services: true do
end
it 'creates system note about merge_request reassign' do
- note = find_note('Reassigned to')
+ note = find_note('assigned to')
expect(note).not_to be_nil
- expect(note.note).to include "Reassigned to \@#{user2.username}"
+ expect(note.note).to include "assigned to #{user2.to_reference}"
end
it 'creates system note about merge_request label edit' do
- note = find_note('Added ~')
+ note = find_note('added ~')
expect(note).not_to be_nil
- expect(note.note).to include "Added ~#{label.id} label"
+ expect(note.note).to include "added #{label.to_reference} label"
end
it 'creates system note about title change' do
- note = find_note('Changed title:')
+ note = find_note('changed title')
expect(note).not_to be_nil
- expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**'
+ expect(note.note).to eq 'changed title from **{-Old-} title** to **{+New+} title**'
end
it 'creates system note about branch change' do
- note = find_note('Target')
+ note = find_note('changed target')
expect(note).not_to be_nil
- expect(note.note).to eq 'Target branch changed from `master` to `target`'
+ expect(note.note).to eq 'changed target branch from `master` to `target`'
end
context 'when not including source branch removal options' do
@@ -199,7 +201,7 @@ describe MergeRequests::UpdateService, services: true do
context 'when the issue is relabeled' do
let!(:non_subscriber) { create(:user) }
- let!(:subscriber) { create(:user).tap { |u| label.toggle_subscription(u) } }
+ let!(:subscriber) { create(:user) { |u| label.toggle_subscription(u, project) } }
before do
project.team << [non_subscriber, :developer]
@@ -258,8 +260,8 @@ describe MergeRequests::UpdateService, services: true do
before { update_merge_request({ description: "- [x] Task 1\n- [X] Task 2" }) }
it 'creates system note about task status change' do
- note1 = find_note('Marked the task **Task 1** as completed')
- note2 = find_note('Marked the task **Task 2** as completed')
+ note1 = find_note('marked the task **Task 1** as completed')
+ note2 = find_note('marked the task **Task 2** as completed')
expect(note1).not_to be_nil
expect(note2).not_to be_nil
@@ -273,8 +275,8 @@ describe MergeRequests::UpdateService, services: true do
end
it 'creates system note about task status change' do
- note1 = find_note('Marked the task **Task 1** as incomplete')
- note2 = find_note('Marked the task **Task 2** as incomplete')
+ note1 = find_note('marked the task **Task 1** as incomplete')
+ note2 = find_note('marked the task **Task 2** as incomplete')
expect(note1).not_to be_nil
expect(note2).not_to be_nil
@@ -318,5 +320,10 @@ describe MergeRequests::UpdateService, services: true do
expect(issue_ids).to be_empty
end
end
+
+ include_examples 'issuable update service' do
+ let(:open_issuable) { merge_request }
+ let(:closed_issuable) { create(:closed_merge_request, source_project: project) }
+ end
end
end
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 93885c84dc3..25804696d2e 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -14,12 +14,41 @@ describe Notes::CreateService, services: true do
end
context "valid params" do
- before do
- @note = Notes::CreateService.new(project, user, opts).execute
+ it 'returns a valid note' do
+ note = Notes::CreateService.new(project, user, opts).execute
+
+ expect(note).to be_valid
+ end
+
+ it 'returns a persisted note' do
+ note = Notes::CreateService.new(project, user, opts).execute
+
+ expect(note).to be_persisted
+ end
+
+ it 'note has valid content' do
+ note = Notes::CreateService.new(project, user, opts).execute
+
+ expect(note.note).to eq(opts[:note])
end
- it { expect(@note).to be_valid }
- it { expect(@note.note).to eq(opts[:note]) }
+ it 'TodoService#new_note is called' do
+ note = build(:note)
+ allow(project).to receive_message_chain(:notes, :new).with(opts) { note }
+
+ expect_any_instance_of(TodoService).to receive(:new_note).with(note, user)
+
+ Notes::CreateService.new(project, user, opts).execute
+ end
+
+ it 'enqueues NewNoteWorker' do
+ note = build(:note, id: 999)
+ allow(project).to receive_message_chain(:notes, :new).with(opts) { note }
+
+ expect(NewNoteWorker).to receive(:perform_async).with(note.id)
+
+ Notes::CreateService.new(project, user, opts).execute
+ end
end
describe 'note with commands' do
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 699b9925b4e..f3e80ac22a0 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe NotificationService, services: true do
+ include EmailHelpers
+
let(:notification) { NotificationService.new }
around(:each) do |example|
@@ -17,7 +19,7 @@ describe NotificationService, services: true do
it 'sends no emails when no new mentions are present' do
send_notifications
- expect(ActionMailer::Base.deliveries).to be_empty
+ should_not_email_anyone
end
it 'emails new mentions with a watch level higher than participant' do
@@ -27,7 +29,7 @@ describe NotificationService, services: true do
it 'does not email new mentions with a watch level equal to or less than participant' do
send_notifications(@u_participating, @u_mentioned)
- expect(ActionMailer::Base.deliveries).to be_empty
+ should_not_email_anyone
end
end
@@ -64,9 +66,9 @@ describe NotificationService, services: true do
before do
build_team(note.project)
- project.team << [issue.author, :master]
- project.team << [issue.assignee, :master]
- project.team << [note.author, :master]
+ project.add_master(issue.author)
+ project.add_master(issue.assignee)
+ project.add_master(note.author)
create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@subscribed_participant cc this guy')
update_custom_notification(:new_note, @u_guest_custom, project)
update_custom_notification(:new_note, @u_custom_global)
@@ -79,7 +81,7 @@ describe NotificationService, services: true do
# Ensure create SentNotification by noteable = issue 6 times, not noteable = note
expect(SentNotification).to receive(:record).with(issue, any_args).exactly(8).times
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
notification.new_note(note)
@@ -111,7 +113,7 @@ describe NotificationService, services: true do
context 'participating' do
context 'by note' do
before do
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
note.author = @u_lazy_participant
note.save
notification.new_note(note)
@@ -134,7 +136,7 @@ describe NotificationService, services: true do
@u_watcher.notification_settings_for(note.project).participating!
@u_watcher.notification_settings_for(note.project.group).global!
update_custom_notification(:new_note, @u_custom_global)
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
end
it do
@@ -168,12 +170,12 @@ describe NotificationService, services: true do
let(:guest_watcher) { create_user_with_notification(:watch, "guest-watcher-confidential") }
it 'filters out users that can not read the issue' do
- project.team << [member, :developer]
- project.team << [guest, :guest]
+ project.add_developer(member)
+ project.add_guest(guest)
expect(SentNotification).to receive(:record).with(confidential_issue, any_args).exactly(4).times
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
notification.new_note(note)
@@ -195,8 +197,8 @@ describe NotificationService, services: true do
before do
build_team(note.project)
- note.project.team << [note.author, :master]
- ActionMailer::Base.deliveries.clear
+ note.project.add_master(note.author)
+ reset_delivered_emails!
end
describe '#new_note' do
@@ -237,8 +239,8 @@ describe NotificationService, services: true do
before do
build_team(note.project)
- note.project.team << [note.author, :master]
- ActionMailer::Base.deliveries.clear
+ note.project.add_master(note.author)
+ reset_delivered_emails!
end
describe '#new_note' do
@@ -273,7 +275,7 @@ describe NotificationService, services: true do
before do
build_team(note.project)
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
allow_any_instance_of(Commit).to receive(:author).and_return(@u_committer)
update_custom_notification(:new_note, @u_guest_custom, project)
update_custom_notification(:new_note, @u_custom_global)
@@ -324,8 +326,8 @@ describe NotificationService, services: true do
before do
build_team(note.project)
- project.team << [merge_request.author, :master]
- project.team << [merge_request.assignee, :master]
+ project.add_master(merge_request.author)
+ project.add_master(merge_request.assignee)
end
describe '#new_note' do
@@ -342,13 +344,15 @@ describe NotificationService, services: true do
end
describe 'Issues' do
- let(:project) { create(:empty_project, :public) }
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, :public, namespace: group) }
+ let(:another_project) { create(:empty_project, :public, namespace: group) }
let(:issue) { create :issue, project: project, assignee: create(:user), description: 'cc @participant' }
before do
build_team(issue.project)
add_users_with_subscription(issue.project, issue)
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
update_custom_notification(:new_issue, @u_guest_custom, project)
update_custom_notification(:new_issue, @u_custom_global)
end
@@ -377,13 +381,24 @@ describe NotificationService, services: true do
end
it "emails subscribers of the issue's labels" do
- subscriber = create(:user)
- label = create(:label, issues: [issue])
+ user_1 = create(:user)
+ user_2 = create(:user)
+ user_3 = create(:user)
+ user_4 = create(:user)
+ label = create(:label, project: project, issues: [issue])
+ group_label = create(:group_label, group: group, issues: [issue])
issue.reload
- label.toggle_subscription(subscriber)
+ label.toggle_subscription(user_1, project)
+ group_label.toggle_subscription(user_2, project)
+ group_label.toggle_subscription(user_3, another_project)
+ group_label.toggle_subscription(user_4)
+
notification.new_issue(issue, @u_disabled)
- should_email(subscriber)
+ should_email(user_1)
+ should_email(user_2)
+ should_not_email(user_3)
+ should_email(user_4)
end
context 'confidential issues' do
@@ -396,19 +411,19 @@ describe NotificationService, services: true do
let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
it "emails subscribers of the issue's labels that can read the issue" do
- project.team << [member, :developer]
- project.team << [guest, :guest]
+ project.add_developer(member)
+ project.add_guest(guest)
- label = create(:label, issues: [confidential_issue])
+ label = create(:label, project: project, issues: [confidential_issue])
confidential_issue.reload
- label.toggle_subscription(non_member)
- label.toggle_subscription(author)
- label.toggle_subscription(assignee)
- label.toggle_subscription(member)
- label.toggle_subscription(guest)
- label.toggle_subscription(admin)
+ label.toggle_subscription(non_member, project)
+ label.toggle_subscription(author, project)
+ label.toggle_subscription(assignee, project)
+ label.toggle_subscription(member, project)
+ label.toggle_subscription(guest, project)
+ label.toggle_subscription(admin, project)
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
notification.new_issue(confidential_issue, @u_disabled)
@@ -554,20 +569,30 @@ describe NotificationService, services: true do
end
describe '#relabeled_issue' do
- let(:label) { create(:label, issues: [issue]) }
- let(:label2) { create(:label) }
- let!(:subscriber_to_label) { create(:user).tap { |u| label.toggle_subscription(u) } }
- let!(:subscriber_to_label2) { create(:user).tap { |u| label2.toggle_subscription(u) } }
+ let(:group_label_1) { create(:group_label, group: group, title: 'Group Label 1', issues: [issue]) }
+ let(:group_label_2) { create(:group_label, group: group, title: 'Group Label 2') }
+ let(:label_1) { create(:label, project: project, title: 'Label 1', issues: [issue]) }
+ let(:label_2) { create(:label, project: project, title: 'Label 2') }
+ let!(:subscriber_to_group_label_1) { create(:user) { |u| group_label_1.toggle_subscription(u, project) } }
+ let!(:subscriber_1_to_group_label_2) { create(:user) { |u| group_label_2.toggle_subscription(u, project) } }
+ let!(:subscriber_2_to_group_label_2) { create(:user) { |u| group_label_2.toggle_subscription(u) } }
+ let!(:subscriber_to_group_label_2_on_another_project) { create(:user) { |u| group_label_2.toggle_subscription(u, another_project) } }
+ let!(:subscriber_to_label_1) { create(:user) { |u| label_1.toggle_subscription(u, project) } }
+ let!(:subscriber_to_label_2) { create(:user) { |u| label_2.toggle_subscription(u, project) } }
it "emails subscribers of the issue's added labels only" do
- notification.relabeled_issue(issue, [label2], @u_disabled)
-
- should_not_email(subscriber_to_label)
- should_email(subscriber_to_label2)
+ notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled)
+
+ should_not_email(subscriber_to_label_1)
+ should_not_email(subscriber_to_group_label_1)
+ should_not_email(subscriber_to_group_label_2_on_another_project)
+ should_email(subscriber_1_to_group_label_2)
+ should_email(subscriber_2_to_group_label_2)
+ should_email(subscriber_to_label_2)
end
it "doesn't send email to anyone but subscribers of the given labels" do
- notification.relabeled_issue(issue, [label2], @u_disabled)
+ notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled)
should_not_email(issue.assignee)
should_not_email(issue.author)
@@ -578,8 +603,12 @@ describe NotificationService, services: true do
should_not_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
- should_not_email(subscriber_to_label)
- should_email(subscriber_to_label2)
+ should_not_email(subscriber_to_label_1)
+ should_not_email(subscriber_to_group_label_1)
+ should_not_email(subscriber_to_group_label_2_on_another_project)
+ should_email(subscriber_1_to_group_label_2)
+ should_email(subscriber_2_to_group_label_2)
+ should_email(subscriber_to_label_2)
end
context 'confidential issues' do
@@ -590,21 +619,21 @@ describe NotificationService, services: true do
let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
- let!(:label_1) { create(:label, issues: [confidential_issue]) }
- let!(:label_2) { create(:label) }
+ let!(:label_1) { create(:label, project: project, issues: [confidential_issue]) }
+ let!(:label_2) { create(:label, project: project) }
it "emails subscribers of the issue's labels that can read the issue" do
- project.team << [member, :developer]
- project.team << [guest, :guest]
+ project.add_developer(member)
+ project.add_guest(guest)
- label_2.toggle_subscription(non_member)
- label_2.toggle_subscription(author)
- label_2.toggle_subscription(assignee)
- label_2.toggle_subscription(member)
- label_2.toggle_subscription(guest)
- label_2.toggle_subscription(admin)
+ label_2.toggle_subscription(non_member, project)
+ label_2.toggle_subscription(author, project)
+ label_2.toggle_subscription(assignee, project)
+ label_2.toggle_subscription(member, project)
+ label_2.toggle_subscription(guest, project)
+ label_2.toggle_subscription(admin, project)
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
notification.relabeled_issue(confidential_issue, [label_2], @u_disabled)
@@ -725,7 +754,9 @@ describe NotificationService, services: true do
end
describe 'Merge Requests' do
- let(:project) { create(:project, :public) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, :public, namespace: group) }
+ let(:another_project) { create(:empty_project, :public, namespace: group) }
let(:merge_request) { create :merge_request, source_project: project, assignee: create(:user), description: 'cc @participant' }
before do
@@ -733,7 +764,7 @@ describe NotificationService, services: true do
add_users_with_subscription(merge_request.target_project, merge_request)
update_custom_notification(:new_merge_request, @u_guest_custom, project)
update_custom_notification(:new_merge_request, @u_custom_global)
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
end
describe '#new_merge_request' do
@@ -758,12 +789,23 @@ describe NotificationService, services: true do
end
it "emails subscribers of the merge request's labels" do
- subscriber = create(:user)
- label = create(:label, merge_requests: [merge_request])
- label.toggle_subscription(subscriber)
+ user_1 = create(:user)
+ user_2 = create(:user)
+ user_3 = create(:user)
+ user_4 = create(:user)
+ label = create(:label, project: project, merge_requests: [merge_request])
+ group_label = create(:group_label, group: group, merge_requests: [merge_request])
+ label.toggle_subscription(user_1, project)
+ group_label.toggle_subscription(user_2, project)
+ group_label.toggle_subscription(user_3, another_project)
+ group_label.toggle_subscription(user_4)
+
notification.new_merge_request(merge_request, @u_disabled)
- should_email(subscriber)
+ should_email(user_1)
+ should_email(user_2)
+ should_not_email(user_3)
+ should_email(user_4)
end
context 'participating' do
@@ -857,20 +899,30 @@ describe NotificationService, services: true do
end
describe '#relabel_merge_request' do
- let(:label) { create(:label, merge_requests: [merge_request]) }
- let(:label2) { create(:label) }
- let!(:subscriber_to_label) { create(:user).tap { |u| label.toggle_subscription(u) } }
- let!(:subscriber_to_label2) { create(:user).tap { |u| label2.toggle_subscription(u) } }
+ let(:group_label_1) { create(:group_label, group: group, title: 'Group Label 1', merge_requests: [merge_request]) }
+ let(:group_label_2) { create(:group_label, group: group, title: 'Group Label 2') }
+ let(:label_1) { create(:label, project: project, title: 'Label 1', merge_requests: [merge_request]) }
+ let(:label_2) { create(:label, project: project, title: 'Label 2') }
+ let!(:subscriber_to_group_label_1) { create(:user) { |u| group_label_1.toggle_subscription(u, project) } }
+ let!(:subscriber_1_to_group_label_2) { create(:user) { |u| group_label_2.toggle_subscription(u, project) } }
+ let!(:subscriber_2_to_group_label_2) { create(:user) { |u| group_label_2.toggle_subscription(u) } }
+ let!(:subscriber_to_group_label_2_on_another_project) { create(:user) { |u| group_label_2.toggle_subscription(u, another_project) } }
+ let!(:subscriber_to_label_1) { create(:user) { |u| label_1.toggle_subscription(u, project) } }
+ let!(:subscriber_to_label_2) { create(:user) { |u| label_2.toggle_subscription(u, project) } }
it "emails subscribers of the merge request's added labels only" do
- notification.relabeled_merge_request(merge_request, [label2], @u_disabled)
-
- should_not_email(subscriber_to_label)
- should_email(subscriber_to_label2)
+ notification.relabeled_merge_request(merge_request, [group_label_2, label_2], @u_disabled)
+
+ should_not_email(subscriber_to_label_1)
+ should_not_email(subscriber_to_group_label_1)
+ should_not_email(subscriber_to_group_label_2_on_another_project)
+ should_email(subscriber_1_to_group_label_2)
+ should_email(subscriber_2_to_group_label_2)
+ should_email(subscriber_to_label_2)
end
it "doesn't send email to anyone but subscribers of the given labels" do
- notification.relabeled_merge_request(merge_request, [label2], @u_disabled)
+ notification.relabeled_merge_request(merge_request, [group_label_2, label_2], @u_disabled)
should_not_email(merge_request.assignee)
should_not_email(merge_request.author)
@@ -881,8 +933,12 @@ describe NotificationService, services: true do
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_lazy_participant)
- should_not_email(subscriber_to_label)
- should_email(subscriber_to_label2)
+ should_not_email(subscriber_to_label_1)
+ should_not_email(subscriber_to_group_label_1)
+ should_not_email(subscriber_to_group_label_2_on_another_project)
+ should_email(subscriber_1_to_group_label_2)
+ should_email(subscriber_2_to_group_label_2)
+ should_email(subscriber_to_label_2)
end
end
@@ -1111,7 +1167,7 @@ describe NotificationService, services: true do
before do
build_team(project)
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
end
describe '#project_was_moved' do
@@ -1156,7 +1212,7 @@ describe NotificationService, services: true do
let(:member) { create(:user) }
before(:each) do
- project.team << [member, :developer, project.owner]
+ project.add_developer(member, current_user: project.owner)
end
it do
@@ -1179,9 +1235,9 @@ describe NotificationService, services: true do
let(:note) { create(:note, noteable: merge_request, project: private_project) }
before do
- private_project.team << [assignee, :developer]
- private_project.team << [developer, :developer]
- private_project.team << [guest, :guest]
+ private_project.add_developer(assignee)
+ private_project.add_developer(developer)
+ private_project.add_guest(guest)
ActionMailer::Base.deliveries.clear
end
@@ -1243,15 +1299,15 @@ describe NotificationService, services: true do
@u_guest_watcher = create_user_with_notification(:watch, 'guest_watching')
@u_guest_custom = create_user_with_notification(:custom, 'guest_custom')
- project.team << [@u_watcher, :master]
- project.team << [@u_participating, :master]
- project.team << [@u_participant_mentioned, :master]
- project.team << [@u_disabled, :master]
- project.team << [@u_mentioned, :master]
- project.team << [@u_committer, :master]
- project.team << [@u_not_mentioned, :master]
- project.team << [@u_lazy_participant, :master]
- project.team << [@u_custom_global, :master]
+ project.add_master(@u_watcher)
+ project.add_master(@u_participating)
+ project.add_master(@u_participant_mentioned)
+ project.add_master(@u_disabled)
+ project.add_master(@u_mentioned)
+ project.add_master(@u_committer)
+ project.add_master(@u_not_mentioned)
+ project.add_master(@u_lazy_participant)
+ project.add_master(@u_custom_global)
end
def create_global_setting_for(user, level)
@@ -1285,15 +1341,15 @@ describe NotificationService, services: true do
@subscribed_participant = create_global_setting_for(create(:user, username: 'subscribed_participant'), :participating)
@watcher_and_subscriber = create_global_setting_for(create(:user), :watch)
- project.team << [@subscribed_participant, :master]
- project.team << [@subscriber, :master]
- project.team << [@unsubscriber, :master]
- project.team << [@watcher_and_subscriber, :master]
+ project.add_master(@subscribed_participant)
+ project.add_master(@subscriber)
+ project.add_master(@unsubscriber)
+ project.add_master(@watcher_and_subscriber)
- issuable.subscriptions.create(user: @subscriber, subscribed: true)
- issuable.subscriptions.create(user: @subscribed_participant, subscribed: true)
- issuable.subscriptions.create(user: @unsubscriber, subscribed: false)
+ issuable.subscriptions.create(user: @subscriber, project: project, subscribed: true)
+ issuable.subscriptions.create(user: @subscribed_participant, project: project, subscribed: true)
+ issuable.subscriptions.create(user: @unsubscriber, project: project, subscribed: false)
# Make the watcher a subscriber to detect dupes
- issuable.subscriptions.create(user: @watcher_and_subscriber, subscribed: true)
+ issuable.subscriptions.create(user: @watcher_and_subscriber, project: project, subscribed: true)
end
end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 876bfaf085c..a1539b69401 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -1,141 +1,150 @@
require 'spec_helper'
-describe Projects::CreateService, services: true do
- describe :create_by_user do
- before do
- @user = create :user
- @opts = {
- name: "GitLab",
- namespace: @user.namespace
- }
- end
+describe Projects::CreateService, '#execute', services: true do
+ let(:user) { create :user }
+ let(:opts) do
+ {
+ name: "GitLab",
+ namespace: user.namespace
+ }
+ end
- it 'creates services on Project creation' do
- project = create_project(@user, @opts)
- project.reload
+ it 'creates labels on Project creation if there are templates' do
+ Label.create(title: "bug", template: true)
+ project = create_project(user, opts)
- expect(project.services).not_to be_empty
- end
+ expect(project.labels).not_to be_empty
+ end
- it 'creates labels on Project creation if there are templates' do
- Label.create(title: "bug", template: true)
- project = create_project(@user, @opts)
- project.reload
+ context 'user namespace' do
+ it do
+ project = create_project(user, opts)
- expect(project.labels).not_to be_empty
+ expect(project).to be_valid
+ expect(project.owner).to eq(user)
+ expect(project.team.masters).to include(user)
+ expect(project.namespace).to eq(user.namespace)
end
+ end
- context 'user namespace' do
- before do
- @project = create_project(@user, @opts)
+ context 'group namespace' do
+ let(:group) do
+ create(:group).tap do |group|
+ group.add_owner(user)
end
-
- it { expect(@project).to be_valid }
- it { expect(@project.owner).to eq(@user) }
- it { expect(@project.team.masters).to include(@user) }
- it { expect(@project.namespace).to eq(@user.namespace) }
end
- context 'group namespace' do
- before do
- @group = create :group
- @group.add_owner(@user)
+ before do
+ user.refresh_authorized_projects # Ensure cache is warm
+ end
- @opts.merge!(namespace_id: @group.id)
- @project = create_project(@user, @opts)
- end
+ it do
+ project = create_project(user, opts.merge!(namespace_id: group.id))
- it { expect(@project).to be_valid }
- it { expect(@project.owner).to eq(@group) }
- it { expect(@project.namespace).to eq(@group) }
+ expect(project).to be_valid
+ expect(project.owner).to eq(group)
+ expect(project.namespace).to eq(group)
+ expect(user.authorized_projects).to include(project)
end
+ end
- context 'error handling' do
- it 'handles invalid options' do
- @opts.merge!({ default_branch: 'master' } )
- expect(create_project(@user, @opts)).to eq(nil)
- end
+ context 'error handling' do
+ it 'handles invalid options' do
+ opts.merge!({ default_branch: 'master' } )
+ expect(create_project(user, opts)).to eq(nil)
end
+ end
- context 'wiki_enabled creates repository directory' do
- context 'wiki_enabled true creates wiki repository directory' do
- before do
- @project = create_project(@user, @opts)
- @path = ProjectWiki.new(@project, @user).send(:path_to_repo)
- end
+ context 'wiki_enabled creates repository directory' do
+ context 'wiki_enabled true creates wiki repository directory' do
+ it do
+ project = create_project(user, opts)
+ path = ProjectWiki.new(project, user).send(:path_to_repo)
- it { expect(File.exist?(@path)).to be_truthy }
+ expect(File.exist?(path)).to be_truthy
end
+ end
- context 'wiki_enabled false does not create wiki repository directory' do
- before do
- @opts.merge!(wiki_enabled: false)
- @project = create_project(@user, @opts)
- @path = ProjectWiki.new(@project, @user).send(:path_to_repo)
- end
+ context 'wiki_enabled false does not create wiki repository directory' do
+ it do
+ opts.merge!(wiki_enabled: false)
+ project = create_project(user, opts)
+ path = ProjectWiki.new(project, user).send(:path_to_repo)
- it { expect(File.exist?(@path)).to be_falsey }
+ expect(File.exist?(path)).to be_falsey
end
end
+ end
- context 'builds_enabled global setting' do
- let(:project) { create_project(@user, @opts) }
-
- subject { project.builds_enabled? }
+ context 'builds_enabled global setting' do
+ let(:project) { create_project(user, opts) }
- context 'global builds_enabled false does not enable CI by default' do
- before do
- project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
- end
+ subject { project.builds_enabled? }
- it { is_expected.to be_falsey }
+ context 'global builds_enabled false does not enable CI by default' do
+ before do
+ project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
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_falsey }
+ end
- it { is_expected.to be_truthy }
+ 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
- context 'restricted visibility level' do
- before do
- stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
+ context 'restricted visibility level' do
+ before do
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
- @opts.merge!(
- visibility_level: Gitlab::VisibilityLevel.options['Public']
- )
- end
+ opts.merge!(
+ visibility_level: Gitlab::VisibilityLevel.options['Public']
+ )
+ end
- it 'does not allow a restricted visibility level for non-admins' do
- project = create_project(@user, @opts)
- expect(project).to respond_to(:errors)
- expect(project.errors.messages).to have_key(:visibility_level)
- expect(project.errors.messages[:visibility_level].first).to(
- match('restricted by your GitLab administrator')
- )
- end
+ it 'does not allow a restricted visibility level for non-admins' do
+ project = create_project(user, opts)
+ expect(project).to respond_to(:errors)
+ expect(project.errors.messages).to have_key(:visibility_level)
+ expect(project.errors.messages[:visibility_level].first).to(
+ match('restricted by your GitLab administrator')
+ )
+ end
- it 'allows a restricted visibility level for admins' do
- admin = create(:admin)
- project = create_project(admin, @opts)
+ it 'allows a restricted visibility level for admins' do
+ admin = create(:admin)
+ project = create_project(admin, opts)
- expect(project.errors.any?).to be(false)
- expect(project.saved?).to be(true)
- end
+ expect(project.errors.any?).to be(false)
+ expect(project.saved?).to be(true)
end
+ end
- context 'repository creation' do
- it 'synchronously creates the repository' do
- expect_any_instance_of(Project).to receive(:create_repository)
+ context 'repository creation' do
+ it 'synchronously creates the repository' do
+ expect_any_instance_of(Project).to receive(:create_repository)
- project = create_project(@user, @opts)
- expect(project).to be_valid
- expect(project.owner).to eq(@user)
- expect(project.namespace).to eq(@user.namespace)
- end
+ project = create_project(user, opts)
+ expect(project).to be_valid
+ expect(project.owner).to eq(user)
+ expect(project.namespace).to eq(user.namespace)
+ end
+ end
+
+ context 'when there is an active service template' do
+ before do
+ create(:service, project: nil, template: true, active: true)
+ end
+
+ it 'creates a service from this template' do
+ project = create_project(user, opts)
+
+ expect(project.services.count).to eq 1
end
end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 7dcd03496bb..90771825f5c 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -7,15 +7,21 @@ describe Projects::DestroyService, services: true do
let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") }
let!(:async) { false } # execute or async_execute
+ shared_examples 'deleting the project' do
+ it 'deletes the project' do
+ expect(Project.all).not_to include(project)
+ expect(Dir.exist?(path)).to be_falsey
+ expect(Dir.exist?(remove_path)).to be_falsey
+ end
+ end
+
context 'Sidekiq inline' do
before do
# Run sidekiq immediatly to check that renamed repository will be removed
Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
end
- it { expect(Project.all).not_to include(project) }
- it { expect(Dir.exist?(path)).to be_falsey }
- it { expect(Dir.exist?(remove_path)).to be_falsey }
+ it_behaves_like 'deleting the project'
end
context 'Sidekiq fake' do
@@ -38,11 +44,21 @@ describe Projects::DestroyService, services: true do
Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
end
- it 'deletes the project' do
- expect(Project.all).not_to include(project)
- expect(Dir.exist?(path)).to be_falsey
- expect(Dir.exist?(remove_path)).to be_falsey
+ it_behaves_like 'deleting the project'
+ 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'
end
context 'container registry' do
diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb
index b57e338b782..becf627a4f5 100644
--- a/spec/services/slash_commands/interpret_service_spec.rb
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -169,7 +169,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'unsubscribe command' do
it 'populates subscription_event: "unsubscribe" if content contains /unsubscribe' do
- issuable.subscribe(developer)
+ issuable.subscribe(developer, project)
_, updates = service.execute(content, issuable)
expect(updates).to eq(subscription_event: 'unsubscribe')
@@ -321,7 +321,7 @@ describe SlashCommands::InterpretService, services: true do
it_behaves_like 'multiple label with same argument' do
let(:content) { %(/label ~"#{inprogress.title}" \n/label ~#{inprogress.title}) }
let(:issuable) { issue }
- end
+ end
it_behaves_like 'unlabel command' do
let(:content) { %(/unlabel ~"#{inprogress.title}") }
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 5bb107fdd85..0e8adb68721 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe SystemNoteService, services: true do
+ include Gitlab::Routing.url_helpers
+
let(:project) { create(:project) }
let(:author) { create(:user) }
let(:noteable) { create(:issue, project: project) }
@@ -48,7 +50,7 @@ describe SystemNoteService, services: true do
context 'without existing commits' do
it 'adds a message header' do
- expect(note_lines[0]).to eq "Added #{new_commits.size} commits:"
+ expect(note_lines[0]).to eq "added #{new_commits.size} commits"
end
it 'adds a message line for each commit' do
@@ -118,7 +120,7 @@ describe SystemNoteService, services: true do
context 'when assignee added' do
it 'sets the note text' do
- expect(subject.note).to eq "Reassigned to @#{assignee.username}"
+ expect(subject.note).to eq "assigned to @#{assignee.username}"
end
end
@@ -126,7 +128,7 @@ describe SystemNoteService, services: true do
let(:assignee) { nil }
it 'sets the note text' do
- expect(subject.note).to eq 'Assignee removed'
+ expect(subject.note).to eq 'removed assignee'
end
end
end
@@ -145,7 +147,7 @@ describe SystemNoteService, services: true do
let(:removed) { [] }
it 'sets the note text' do
- expect(subject.note).to eq "Added ~#{labels[0].id} ~#{labels[1].id} labels"
+ expect(subject.note).to eq "added ~#{labels[0].id} ~#{labels[1].id} labels"
end
end
@@ -154,7 +156,7 @@ describe SystemNoteService, services: true do
let(:removed) { labels }
it 'sets the note text' do
- expect(subject.note).to eq "Removed ~#{labels[0].id} ~#{labels[1].id} labels"
+ expect(subject.note).to eq "removed ~#{labels[0].id} ~#{labels[1].id} labels"
end
end
@@ -163,7 +165,7 @@ describe SystemNoteService, services: true do
let(:removed) { [labels[1]] }
it 'sets the note text' do
- expect(subject.note).to eq "Added ~#{labels[0].id} and removed ~#{labels[1].id} labels"
+ expect(subject.note).to eq "added ~#{labels[0].id} and removed ~#{labels[1].id} labels"
end
end
end
@@ -177,7 +179,7 @@ describe SystemNoteService, services: true do
context 'when milestone added' do
it 'sets the note text' do
- expect(subject.note).to eq "Milestone changed to #{milestone.to_reference}"
+ expect(subject.note).to eq "changed milestone to #{milestone.to_reference}"
end
end
@@ -185,7 +187,7 @@ describe SystemNoteService, services: true do
let(:milestone) { nil }
it 'sets the note text' do
- expect(subject.note).to eq 'Milestone removed'
+ expect(subject.note).to eq 'removed milestone'
end
end
end
@@ -202,13 +204,13 @@ describe SystemNoteService, services: true do
let(:source) { double('commit', gfm_reference: 'commit 123456') }
it 'sets the note text' do
- expect(subject.note).to eq "Status changed to #{status} by commit 123456"
+ expect(subject.note).to eq "#{status} via commit 123456"
end
end
context 'without a source' do
it 'sets the note text' do
- expect(subject.note).to eq "Status changed to #{status}"
+ expect(subject.note).to eq status
end
end
end
@@ -223,8 +225,8 @@ describe SystemNoteService, services: true do
it_behaves_like 'a system note'
- it "posts the Merge When Build Succeeds system note" do
- expect(subject.note).to match /Enabled an automatic merge when the build for (\w+\/\w+@)?[0-9a-f]{40} succeeds/
+ it "posts the 'merge when pipeline succeeds' system note" do
+ expect(subject.note).to match /enabled an automatic merge when the pipeline for (\w+\/\w+@)?\h{40} succeeds/
end
end
@@ -237,8 +239,8 @@ describe SystemNoteService, services: true do
it_behaves_like 'a system note'
- it "posts the Merge When Build Succeeds system note" do
- expect(subject.note).to eq "Canceled the automatic merge"
+ it "posts the 'merge when pipeline succeeds' system note" do
+ expect(subject.note).to eq "canceled the automatic merge"
end
end
@@ -250,7 +252,7 @@ describe SystemNoteService, services: true do
it 'sets the note text' do
expect(subject.note).
- to eq "Changed title: **{-Old title-}** → **{+#{noteable.title}+}**"
+ to eq "changed title from **{-Old title-}** to **{+#{noteable.title}+}**"
end
end
end
@@ -262,7 +264,7 @@ describe SystemNoteService, services: true do
it_behaves_like 'a system note'
it 'sets the note text' do
- expect(subject.note).to eq 'Made the issue visible'
+ expect(subject.note).to eq 'made the issue visible to everyone'
end
end
end
@@ -276,7 +278,7 @@ describe SystemNoteService, services: true do
context 'when target branch name changed' do
it 'sets the note text' do
- expect(subject.note).to eq "Target branch changed from `#{old_branch}` to `#{new_branch}`"
+ expect(subject.note).to eq "changed target branch from `#{old_branch}` to `#{new_branch}`"
end
end
end
@@ -288,7 +290,7 @@ describe SystemNoteService, services: true do
context 'when source branch deleted' do
it 'sets the note text' do
- expect(subject.note).to eq "Deleted source branch `feature`"
+ expect(subject.note).to eq "deleted source branch `feature`"
end
end
end
@@ -300,7 +302,7 @@ describe SystemNoteService, services: true do
context 'when a branch is created from the new branch button' do
it 'sets the note text' do
- expect(subject.note).to match /\AStarted branch [`1-mepmep`]/
+ expect(subject.note).to match /\Acreated branch [`1-mepmep`]/
end
end
end
@@ -336,13 +338,13 @@ describe SystemNoteService, services: true do
let(:mentioner) { project2.repository.commit }
it 'references the mentioning commit' do
- expect(subject.note).to eq "Mentioned in commit #{mentioner.to_reference(project)}"
+ expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference(project)}"
end
end
context 'from non-Commit' do
it 'references the mentioning object' do
- expect(subject.note).to eq "Mentioned in issue #{mentioner.to_reference(project)}"
+ expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference(project)}"
end
end
end
@@ -352,13 +354,13 @@ describe SystemNoteService, services: true do
let(:mentioner) { project.repository.commit }
it 'references the mentioning commit' do
- expect(subject.note).to eq "Mentioned in commit #{mentioner.to_reference}"
+ expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference}"
end
end
context 'from non-Commit' do
it 'references the mentioning object' do
- expect(subject.note).to eq "Mentioned in issue #{mentioner.to_reference}"
+ expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference}"
end
end
end
@@ -368,7 +370,11 @@ describe SystemNoteService, services: true do
describe '.cross_reference?' do
it 'is truthy when text begins with expected text' do
- expect(described_class.cross_reference?('Mentioned in something')).to be_truthy
+ expect(described_class.cross_reference?('mentioned in something')).to be_truthy
+ end
+
+ it 'is truthy when text begins with legacy capitalized expected text' do
+ expect(described_class.cross_reference?('mentioned in something')).to be_truthy
end
it 'is falsey when text does not begin with expected text' do
@@ -431,6 +437,19 @@ describe SystemNoteService, services: true do
expect(described_class.cross_reference_exists?(noteable, commit1)).
to be_falsey
end
+
+ context 'legacy capitalized cross reference' do
+ before do
+ # Mention issue (noteable) from commit0
+ system_note = described_class.cross_reference(noteable, commit0, author)
+ system_note.update(note: system_note.note.capitalize)
+ end
+
+ it 'is truthy when already mentioned' do
+ expect(described_class.cross_reference_exists?(noteable, commit0)).
+ to be_truthy
+ end
+ end
end
context 'commit from commit' do
@@ -448,6 +467,19 @@ describe SystemNoteService, services: true do
expect(described_class.cross_reference_exists?(commit1, commit0)).
to be_falsey
end
+
+ context 'legacy capitalized cross reference' do
+ before do
+ # Mention commit1 from commit0
+ system_note = described_class.cross_reference(commit0, commit1, author)
+ system_note.update(note: system_note.note.capitalize)
+ end
+
+ it 'is truthy when already mentioned' do
+ expect(described_class.cross_reference_exists?(commit0, commit1)).
+ to be_truthy
+ end
+ end
end
context 'commit with cross-reference from fork' do
@@ -463,6 +495,18 @@ describe SystemNoteService, services: true do
expect(described_class.cross_reference_exists?(noteable, commit2)).
to be true
end
+
+ context 'legacy capitalized cross reference' do
+ before do
+ system_note = described_class.cross_reference(noteable, commit0, author2)
+ system_note.update(note: system_note.note.capitalize)
+ end
+
+ it 'is true when a fork mentions an external issue' do
+ expect(described_class.cross_reference_exists?(noteable, commit2)).
+ to be true
+ end
+ end
end
end
@@ -486,7 +530,7 @@ describe SystemNoteService, services: true do
end
it 'mentions referenced project' do
- expect(subject.note).to include new_project.to_reference
+ expect(subject.note).to include new_project.path_with_namespace
end
end
@@ -496,7 +540,7 @@ describe SystemNoteService, services: true do
it_behaves_like 'cross project mentionable'
it 'notifies about noteable being moved to' do
- expect(subject.note).to match /Moved to/
+ expect(subject.note).to match /moved to/
end
end
@@ -506,7 +550,7 @@ describe SystemNoteService, services: true do
it_behaves_like 'cross project mentionable'
it 'notifies about noteable being moved from' do
- expect(subject.note).to match /Moved from/
+ expect(subject.note).to match /moved from/
end
end
@@ -534,44 +578,166 @@ describe SystemNoteService, services: true do
let(:project) { create(:jira_project) }
let(:author) { create(:user) }
let(:issue) { create(:issue, project: project) }
- let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) }
+ let(:merge_request) { create(:merge_request, :simple, target_project: project, source_project: project) }
let(:jira_issue) { ExternalIssue.new("JIRA-1", project)}
let(:jira_tracker) { project.jira_service }
let(:commit) { project.commit }
let(:comment_url) { jira_api_comment_url(jira_issue.id) }
let(:success_message) { "JiraService SUCCESS: Successfully posted to http://jira.example.net." }
- before { stub_jira_urls(jira_issue.id) }
+ before do
+ stub_jira_urls(jira_issue.id)
+ jira_service_settings
+ end
+
+ noteable_types = ["merge_requests", "commit"]
+
+ noteable_types.each do |type|
+ context "when noteable is a #{type}" do
+ it "blocks cross reference when #{type.underscore}_events is false" do
+ jira_tracker.update("#{type}_events" => false)
- context 'in JIRA issue tracker' do
- before { jira_service_settings }
+ noteable = type == "commit" ? commit : merge_request
+ result = described_class.cross_reference(jira_issue, noteable, author)
- describe "new reference" do
- subject { described_class.cross_reference(jira_issue, commit, author) }
+ expect(result).to eq("Events for #{noteable.class.to_s.underscore.humanize.pluralize.downcase} are disabled.")
+ end
+
+ it "blocks cross reference when #{type.underscore}_events is true" do
+ jira_tracker.update("#{type}_events" => true)
- it { is_expected.to eq(success_message) }
+ noteable = type == "commit" ? commit : merge_request
+ result = described_class.cross_reference(jira_issue, noteable, author)
+
+ expect(result).to eq(success_message)
+ end
end
end
- context 'issue from an issue' do
- context 'in JIRA issue tracker' do
- before { jira_service_settings }
+ describe "new reference" do
+ context 'for commits' do
+ it "creates comment" do
+ result = described_class.cross_reference(jira_issue, commit, author)
+
+ expect(result).to eq(success_message)
+ end
+
+ it "creates remote link" do
+ described_class.cross_reference(jira_issue, commit, author)
+
+ expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with(
+ body: hash_including(
+ GlobalID: "GitLab",
+ object: {
+ url: namespace_project_commit_url(project.namespace, project, commit),
+ title: "GitLab: Mentioned on commit - #{commit.title}",
+ icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" },
+ status: { resolved: false }
+ }
+ )
+ ).once
+ end
+ end
+
+ context 'for issues' do
+ let(:issue) { create(:issue, project: project) }
+
+ it "creates comment" do
+ result = described_class.cross_reference(jira_issue, issue, author)
+
+ expect(result).to eq(success_message)
+ end
+
+ it "creates remote link" do
+ described_class.cross_reference(jira_issue, issue, author)
+
+ expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with(
+ body: hash_including(
+ GlobalID: "GitLab",
+ object: {
+ url: namespace_project_issue_url(project.namespace, project, issue),
+ title: "GitLab: Mentioned on issue - #{issue.title}",
+ icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" },
+ status: { resolved: false }
+ }
+ )
+ ).once
+ end
+ end
- subject { described_class.cross_reference(jira_issue, issue, author) }
+ context 'for snippets' do
+ let(:snippet) { create(:snippet, project: project) }
- it { is_expected.to eq(success_message) }
+ it "creates comment" do
+ result = described_class.cross_reference(jira_issue, snippet, author)
+
+ expect(result).to eq(success_message)
+ end
+
+ it "creates remote link" do
+ described_class.cross_reference(jira_issue, snippet, author)
+
+ expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with(
+ body: hash_including(
+ GlobalID: "GitLab",
+ object: {
+ url: namespace_project_snippet_url(project.namespace, project, snippet),
+ title: "GitLab: Mentioned on snippet - #{snippet.title}",
+ icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" },
+ status: { resolved: false }
+ }
+ )
+ ).once
+ end
end
end
describe "existing reference" do
before do
- message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\n'#{commit.title}'"
+ message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\n'#{commit.title.chomp}'"
allow_any_instance_of(JIRA::Resource::Issue).to receive(:comments).and_return([OpenStruct.new(body: message)])
end
- subject { described_class.cross_reference(jira_issue, commit, author) }
+ it "does not return success message" do
+ result = described_class.cross_reference(jira_issue, commit, author)
+
+ expect(result).not_to eq(success_message)
+ end
+
+ it 'does not try to create comment and remote link' do
+ subject
+
+ expect(WebMock).not_to have_requested(:post, jira_api_comment_url(jira_issue))
+ expect(WebMock).not_to have_requested(:post, jira_api_remote_link_url(jira_issue))
+ end
+ end
+ end
+
+ describe '.discussion_continued_in_issue' 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 }
+ let(:issue) { create(:issue, project: project) }
+ let(:user) { create(:user) }
+
+ def reloaded_merge_request
+ MergeRequest.find(merge_request.id)
+ end
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'creates a new note in the discussion' do
+ # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded.
+ expect { SystemNoteService.discussion_continued_in_issue(discussion, project, user, issue) }.
+ to change { reloaded_merge_request.discussions.first.notes.size }.by(1)
+ end
+
+ it 'mentions the created issue in the system note' do
+ note = SystemNoteService.discussion_continued_in_issue(discussion, project, user, issue)
- it { is_expected.not_to eq(success_message) }
+ expect(note.note).to include(issue.to_reference)
end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index b2ca856f89f..6ee3307512d 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -26,11 +26,13 @@ RSpec.configure do |config|
config.verbose_retry = true
config.display_try_failure_messages = true
- config.include Devise::Test::ControllerHelpers, type: :controller
+ config.include Devise::Test::ControllerHelpers, type: :controller
+ config.include Devise::Test::ControllerHelpers, type: :view
config.include Warden::Test::Helpers, type: :request
- config.include LoginHelpers, type: :feature
+ config.include LoginHelpers, type: :feature
+ config.include SearchHelpers, type: :feature
config.include StubConfiguration
- config.include EmailHelpers
+ config.include EmailHelpers, type: :mailer
config.include TestEnv
config.include ActiveJob::TestHelper
config.include ActiveSupport::Testing::TimeHelpers
@@ -53,8 +55,12 @@ RSpec.configure do |config|
config.around(:each, :redis) do |example|
Gitlab::Redis.with(&:flushall)
+ Sidekiq.redis(&:flushall)
+
example.run
+
Gitlab::Redis.with(&:flushall)
+ Sidekiq.redis(&:flushall)
end
end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index e1f90e17cce..16d5f2bf0b8 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -7,7 +7,15 @@ timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
- Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout, window_size: [1366, 768])
+ Capybara::Poltergeist::Driver.new(
+ app,
+ js_errors: true,
+ timeout: timeout,
+ window_size: [1366, 768],
+ phantomjs_options: [
+ '--load-images=no'
+ ]
+ )
end
Capybara.default_max_wait_time = timeout
diff --git a/spec/support/carrierwave.rb b/spec/support/carrierwave.rb
index aa89afd8fb3..72af2c70324 100644
--- a/spec/support/carrierwave.rb
+++ b/spec/support/carrierwave.rb
@@ -1,7 +1,7 @@
CarrierWave.root = 'tmp/tests/uploads'
RSpec.configure do |config|
- config.after(:suite) do
+ config.after(:each) do
FileUtils.rm_rf('tmp/tests/uploads')
end
end
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
index 62a5b46d47b..75c95d70951 100644
--- a/spec/support/cycle_analytics_helpers.rb
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -49,7 +49,8 @@ module CycleAnalyticsHelpers
end
def merge_merge_requests_closing_issue(issue)
- merge_requests = issue.closed_by_merge_requests
+ merge_requests = issue.closed_by_merge_requests(user)
+
merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user).execute(merge_request) }
end
diff --git a/spec/support/database_connection_helpers.rb b/spec/support/database_connection_helpers.rb
new file mode 100644
index 00000000000..763329499f0
--- /dev/null
+++ b/spec/support/database_connection_helpers.rb
@@ -0,0 +1,9 @@
+module DatabaseConnectionHelpers
+ def run_with_new_database_connection
+ pool = ActiveRecord::Base.connection_pool
+ conn = pool.checkout
+ yield conn
+ ensure
+ pool.checkin(conn)
+ end
+end
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index ac38e31b77e..247f0954221 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -11,6 +11,10 @@ RSpec.configure do |config|
DatabaseCleaner.strategy = :truncation
end
+ config.before(:each, truncate: true) do
+ DatabaseCleaner.strategy = :truncation
+ end
+
config.before(:each) do
DatabaseCleaner.start
end
diff --git a/spec/support/email_helpers.rb b/spec/support/email_helpers.rb
index 0bfc4685532..3e979f2f470 100644
--- a/spec/support/email_helpers.rb
+++ b/spec/support/email_helpers.rb
@@ -1,23 +1,33 @@
module EmailHelpers
- def sent_to_user?(user)
- ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1
+ def sent_to_user?(user, recipients = email_recipients)
+ recipients.include?(user.notification_email)
end
def reset_delivered_emails!
ActionMailer::Base.deliveries.clear
end
- def should_only_email(*users)
- users.each {|user| should_email(user) }
- recipients = ActionMailer::Base.deliveries.flat_map(&:to)
+ def should_only_email(*users, kind: :to)
+ recipients = email_recipients(kind: kind)
+
+ users.each { |user| should_email(user, recipients) }
+
expect(recipients.count).to eq(users.count)
end
- def should_email(user)
- expect(sent_to_user?(user)).to be_truthy
+ def should_email(user, recipients = email_recipients)
+ expect(sent_to_user?(user, recipients)).to be_truthy
+ end
+
+ def should_not_email(user, recipients = email_recipients)
+ expect(sent_to_user?(user, recipients)).to be_falsey
+ end
+
+ def should_not_email_anyone
+ expect(ActionMailer::Base.deliveries).to be_empty
end
- def should_not_email(user)
- expect(sent_to_user?(user)).to be_falsey
+ def email_recipients(kind: :to)
+ ActionMailer::Base.deliveries.flat_map(&kind)
end
end
diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index 5e3b8f2b23e..194620d0a68 100644
--- a/spec/support/features/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -230,31 +230,31 @@ shared_examples 'issuable record that supports slash commands in its description
context "with a note subscribing to the #{issuable_type}" do
it "creates a new todo for the #{issuable_type}" do
- expect(issuable.subscribed?(master)).to be_falsy
+ expect(issuable.subscribed?(master, project)).to be_falsy
write_note("/subscribe")
expect(page).not_to have_content '/subscribe'
expect(page).to have_content 'Your commands have been executed!'
- expect(issuable.subscribed?(master)).to be_truthy
+ expect(issuable.subscribed?(master, project)).to be_truthy
end
end
context "with a note unsubscribing to the #{issuable_type} as done" do
before do
- issuable.subscribe(master)
+ issuable.subscribe(master, project)
end
it "creates a new todo for the #{issuable_type}" do
- expect(issuable.subscribed?(master)).to be_truthy
+ expect(issuable.subscribed?(master, project)).to be_truthy
write_note("/unsubscribe")
expect(page).not_to have_content '/unsubscribe'
expect(page).to have_content 'Your commands have been executed!'
- expect(issuable.subscribed?(master)).to be_falsy
+ expect(issuable.subscribed?(master, project)).to be_falsy
end
end
end
diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb
index adc3f48b434..99e98eebdb4 100644
--- a/spec/support/javascript_fixtures_helpers.rb
+++ b/spec/support/javascript_fixtures_helpers.rb
@@ -1,3 +1,4 @@
+require 'action_dispatch/testing/test_request'
require 'fileutils'
require 'gitlab/popen'
@@ -30,13 +31,17 @@ module JavaScriptFixturesHelpers
if response_mime_type.html?
doc = Nokogiri::HTML::DocumentFragment.parse(fixture)
+ link_tags = doc.css('link')
+ link_tags.remove
+
scripts = doc.css('script')
scripts.remove
fixture = doc.to_html
# replace relative links
- fixture.gsub!(%r{="/}, '="https://fixture.invalid/')
+ test_host = ActionDispatch::TestRequest::DEFAULT_ENV['HTTP_HOST']
+ fixture.gsub!(%r{="/}, "=\"http://#{test_host}/")
end
FileUtils.mkdir_p(File.dirname(fixture_file_name))
diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb
index 96e0dad6b55..929fc0c5182 100644
--- a/spec/support/jira_service_helper.rb
+++ b/spec/support/jira_service_helper.rb
@@ -6,7 +6,8 @@ module JiraServiceHelper
properties = {
title: "JIRA tracker",
url: JIRA_URL,
- project_key: "JIRA"
+ project_key: "JIRA",
+ jira_issue_transition_id: '1'
}
jira_tracker.update_attributes(properties: properties, active: true)
@@ -57,6 +58,10 @@ module JiraServiceHelper
JIRA_API + "/issue/#{issue_id}/comment"
end
+ def jira_api_remote_link_url(issue_id)
+ JIRA_API + "/issue/#{issue_id}/remotelink"
+ end
+
def jira_api_transition_url(issue_id)
JIRA_API + "/issue/#{issue_id}/transitions"
end
@@ -75,6 +80,7 @@ module JiraServiceHelper
WebMock.stub_request(:get, jira_issue_url(issue_id))
WebMock.stub_request(:get, jira_api_test_url)
WebMock.stub_request(:post, jira_api_comment_url(issue_id))
+ WebMock.stub_request(:post, jira_api_remote_link_url(issue_id))
WebMock.stub_request(:post, jira_api_transition_url(issue_id))
end
end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index c0b3e83244d..ad1eed5b369 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -75,7 +75,8 @@ module LoginHelpers
def logout
find(".header-user-dropdown-toggle").click
click_link "Sign out"
- expect(page).to have_content('Signed out successfully')
+ # check the sign_in button
+ expect(page).to have_button('Sign in')
end
# Logout without JavaScript driver
diff --git a/spec/support/matchers/access_matchers.rb b/spec/support/matchers/access_matchers.rb
index 0497e391860..ceddb656596 100644
--- a/spec/support/matchers/access_matchers.rb
+++ b/spec/support/matchers/access_matchers.rb
@@ -7,7 +7,7 @@ module AccessMatchers
extend RSpec::Matchers::DSL
include Warden::Test::Helpers
- def emulate_user(user)
+ def emulate_user(user, membership = nil)
case user
when :user
login_as(create(:user))
@@ -19,6 +19,19 @@ module AccessMatchers
login_as(create(:user, external: true))
when User
login_as(user)
+ when *Gitlab::Access.sym_options_with_owner.keys
+ raise ArgumentError, "cannot emulate #{user} without membership parent" unless membership
+
+ role = user
+
+ if role == :owner && membership.owner
+ user = membership.owner
+ else
+ user = create(:user)
+ membership.public_send(:"add_#{role}", user)
+ end
+
+ login_as(user)
else
raise ArgumentError, "cannot emulate user #{user}"
end
@@ -26,8 +39,7 @@ module AccessMatchers
def description_for(user, type)
if user.kind_of?(User)
- # User#inspect displays too much information for RSpec's description
- # messages
+ # User#inspect displays too much information for RSpec's descriptions
"be #{type} for the specified user"
else
"be #{type} for #{user}"
@@ -36,21 +48,31 @@ module AccessMatchers
matcher :be_allowed_for do |user|
match do |url|
- emulate_user(user)
- visit url
+ emulate_user(user, @membership)
+ visit(url)
+
status_code != 404 && current_path != new_user_session_path
end
+ chain :of do |membership|
+ @membership = membership
+ end
+
description { description_for(user, 'allowed') }
end
matcher :be_denied_for do |user|
match do |url|
- emulate_user(user)
- visit url
+ emulate_user(user, @membership)
+ visit(url)
+
status_code == 404 || current_path == new_user_session_path
end
+ chain :of do |membership|
+ @membership = membership
+ end
+
description { description_for(user, 'denied') }
end
end
diff --git a/spec/support/matchers/be_url.rb b/spec/support/matchers/be_url.rb
new file mode 100644
index 00000000000..f8096af1b22
--- /dev/null
+++ b/spec/support/matchers/be_url.rb
@@ -0,0 +1,5 @@
+RSpec::Matchers.define :be_url do |_|
+ match do |actual|
+ URI.parse(actual) rescue false
+ end
+end
diff --git a/spec/support/matchers/have_issuable_counts.rb b/spec/support/matchers/have_issuable_counts.rb
index 02605d6b70e..92cf3de5448 100644
--- a/spec/support/matchers/have_issuable_counts.rb
+++ b/spec/support/matchers/have_issuable_counts.rb
@@ -1,9 +1,9 @@
RSpec::Matchers.define :have_issuable_counts do |opts|
- match do |actual|
- expected_counts = opts.map do |state, count|
- "#{state.to_s.humanize} #{count}"
- end
+ expected_counts = opts.map do |state, count|
+ "#{state.to_s.humanize} #{count}"
+ end
+ match do |actual|
actual.within '.issues-state-filters' do
expected_counts.each do |expected_count|
expect(actual).to have_content(expected_count)
diff --git a/spec/support/matchers/is_within.rb b/spec/support/matchers/is_within.rb
deleted file mode 100644
index 0c35fc7e899..00000000000
--- a/spec/support/matchers/is_within.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# Extend shoulda-matchers
-module Shoulda::Matchers::ActiveModel
- class ValidateLengthOfMatcher
- # Shortcut for is_at_least and is_at_most
- def is_within(range)
- is_at_least(range.min) && is_at_most(range.max)
- end
- end
-end
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 8c98b1f988c..97b8b342eb2 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -38,9 +38,9 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('h1 a#gitlab-markdown')
- expect(actual).to have_selector('h2 a#markdown')
- expect(actual).to have_selector('h3 a#autolinkfilter')
+ expect(actual).to have_selector('h1 a#user-content-gitlab-markdown')
+ expect(actual).to have_selector('h2 a#user-content-markdown')
+ expect(actual).to have_selector('h3 a#user-content-autolinkfilter')
end
end
diff --git a/spec/support/notify_shared_examples.rb b/spec/support/notify_shared_examples.rb
index 3956d05060b..49867aa5cc4 100644
--- a/spec/support/notify_shared_examples.rb
+++ b/spec/support/notify_shared_examples.rb
@@ -7,7 +7,7 @@ shared_context 'gitlab email notification' do
let(:new_user_address) { 'newguy@example.com' }
before do
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
email = recipient.emails.create(email: "notifications@example.com")
recipient.update_attribute(:notification_email, email.email)
stub_incoming_email_setting(enabled: true, address: "reply+%{key}@#{Gitlab.config.gitlab.host}")
diff --git a/spec/support/project_features_apply_to_issuables_shared_examples.rb b/spec/support/project_features_apply_to_issuables_shared_examples.rb
new file mode 100644
index 00000000000..4621d17549b
--- /dev/null
+++ b/spec/support/project_features_apply_to_issuables_shared_examples.rb
@@ -0,0 +1,56 @@
+shared_examples 'project features apply to issuables' do |klass|
+ let(:described_class) { klass }
+
+ let(:group) { create(:group) }
+ let(:user_in_group) { create(:group_member, :developer, user: create(:user), group: group ).user }
+ let(:user_outside_group) { create(:user) }
+
+ let(:project) { create(:empty_project, :public, project_args) }
+
+ def project_args
+ feature = "#{described_class.model_name.plural}_access_level".to_sym
+
+ args = { group: group }
+ args[feature] = access_level
+
+ args
+ end
+
+ before do
+ _ = issuable
+ login_as(user)
+ visit path
+ end
+
+ context 'public access level' do
+ let(:access_level) { ProjectFeature::ENABLED }
+
+ context 'group member' do
+ let(:user) { user_in_group }
+
+ it { expect(page).to have_content(issuable.title) }
+ end
+
+ context 'non-member' do
+ let(:user) { user_outside_group }
+
+ it { expect(page).to have_content(issuable.title) }
+ end
+ end
+
+ context 'private access level' do
+ let(:access_level) { ProjectFeature::PRIVATE }
+
+ context 'group member' do
+ let(:user) { user_in_group }
+
+ it { expect(page).to have_content(issuable.title) }
+ end
+
+ context 'non-member' do
+ let(:user) { user_outside_group }
+
+ it { expect(page).not_to have_content(issuable.title) }
+ end
+ end
+end
diff --git a/spec/support/rake_helpers.rb b/spec/support/rake_helpers.rb
index 52d80c69835..4a8158ed79b 100644
--- a/spec/support/rake_helpers.rb
+++ b/spec/support/rake_helpers.rb
@@ -1,7 +1,7 @@
module RakeHelpers
- def run_rake_task(task_name)
+ def run_rake_task(task_name, *args)
Rake::Task[task_name].reenable
- Rake.application.invoke_task task_name
+ Rake.application.invoke_task("#{task_name}[#{args.join(',')}]")
end
def stub_warn_user_is_not_gitlab
diff --git a/spec/support/reference_parser_shared_examples.rb b/spec/support/reference_parser_shared_examples.rb
new file mode 100644
index 00000000000..8eb74635a60
--- /dev/null
+++ b/spec/support/reference_parser_shared_examples.rb
@@ -0,0 +1,43 @@
+RSpec.shared_examples "referenced feature visibility" do |*related_features|
+ let(:feature_fields) do
+ related_features.map { |feature| (feature + "_access_level").to_sym }
+ end
+
+ before { link['data-project'] = project.id.to_s }
+
+ context "when feature is disabled" do
+ it "does not create reference" do
+ set_features_fields_to(ProjectFeature::DISABLED)
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([])
+ end
+ end
+
+ context "when feature is enabled only for team members" do
+ before { set_features_fields_to(ProjectFeature::PRIVATE) }
+
+ it "does not create reference for non member" do
+ non_member = create(:user)
+
+ expect(subject.nodes_visible_to_user(non_member, [link])).to eq([])
+ end
+
+ it "creates reference for member" do
+ project.team << [user, :developer]
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+ end
+
+ context "when feature is enabled" do
+ # The project is public
+ it "creates reference" do
+ set_features_fields_to(ProjectFeature::ENABLED)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+ end
+
+ def set_features_fields_to(visibility_level)
+ feature_fields.each { |field| project.project_feature.update_attribute(field, visibility_level) }
+ end
+end
diff --git a/spec/support/search_helpers.rb b/spec/support/search_helpers.rb
new file mode 100644
index 00000000000..abbbb636d66
--- /dev/null
+++ b/spec/support/search_helpers.rb
@@ -0,0 +1,5 @@
+module SearchHelpers
+ def select_filter(name)
+ find(:xpath, "//ul[contains(@class, 'search-filter')]//a[contains(.,'#{name}')]").click
+ end
+end
diff --git a/spec/support/services/issuable_update_service_shared_examples.rb b/spec/support/services/issuable_update_service_shared_examples.rb
new file mode 100644
index 00000000000..a3336755773
--- /dev/null
+++ b/spec/support/services/issuable_update_service_shared_examples.rb
@@ -0,0 +1,17 @@
+shared_examples 'issuable update service' do
+ context 'changing state' do
+ before { expect(project).to receive(:execute_hooks).once }
+
+ context 'to reopened' do
+ it 'executes hooks only once' do
+ described_class.new(project, user, state_event: 'reopen').execute(closed_issuable)
+ end
+ end
+
+ context 'to closed' do
+ it 'executes hooks only once' do
+ described_class.new(project, user, state_event: 'close').execute(open_issuable)
+ end
+ end
+ end
+end
diff --git a/spec/support/services_shared_context.rb b/spec/support/services_shared_context.rb
index d1c999cad4d..66c93890e31 100644
--- a/spec/support/services_shared_context.rb
+++ b/spec/support/services_shared_context.rb
@@ -16,8 +16,14 @@ Service.available_services_names.each do |service|
hash.merge!(k => 'secrettoken')
elsif k =~ /^(.*_url|url|webhook)/
hash.merge!(k => "http://example.com")
+ elsif service_klass.method_defined?("#{k}?")
+ hash.merge!(k => true)
elsif service == 'irker' && k == :recipients
hash.merge!(k => 'irc://irc.network.net:666/#channel')
+ elsif service == 'irker' && k == :server_port
+ hash.merge!(k => 1234)
+ elsif service == 'jira' && k == :jira_issue_transition_id
+ hash.merge!(k => 1234)
else
hash.merge!(k => "someword")
end
diff --git a/spec/support/setup_builds_storage.rb b/spec/support/setup_builds_storage.rb
index a4f21e95338..2e7c88bfc09 100644
--- a/spec/support/setup_builds_storage.rb
+++ b/spec/support/setup_builds_storage.rb
@@ -1,19 +1,18 @@
RSpec.configure do |config|
def builds_path
- Rails.root.join('tmp/builds')
+ Rails.root.join('tmp/tests/builds')
end
- config.before(:each) do
- FileUtils.mkdir_p(builds_path)
- FileUtils.touch(File.join(builds_path, ".gitkeep"))
+ config.before(:suite) do
Settings.gitlab_ci['builds_path'] = builds_path
end
- config.after(:suite) do
- Dir[File.join(builds_path, '*')].each do |path|
- next if File.basename(path) == '.gitkeep'
+ config.before(:all) do
+ FileUtils.mkdir_p(builds_path)
+ end
- FileUtils.rm_rf(path)
- end
+ config.before(:each) do
+ FileUtils.rm_rf(builds_path)
+ FileUtils.mkdir_p(builds_path)
end
end
diff --git a/spec/support/slack_mattermost_shared_examples.rb b/spec/support/slack_mattermost_shared_examples.rb
new file mode 100644
index 00000000000..56d4965f74d
--- /dev/null
+++ b/spec/support/slack_mattermost_shared_examples.rb
@@ -0,0 +1,328 @@
+Dir[Rails.root.join("app/models/project_services/chat_message/*.rb")].each { |f| require f }
+
+RSpec.shared_examples 'slack or mattermost' do
+ let(:chat_service) { described_class.new }
+ let(:webhook_url) { 'https://example.gitlab.com/' }
+
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to have_one :service_hook }
+ end
+
+ describe 'Validations' do
+ context 'when service is active' do
+ before { subject.active = true }
+
+ it { is_expected.to validate_presence_of(:webhook) }
+ it_behaves_like 'issue tracker service URL attribute', :webhook
+ end
+
+ context 'when service is inactive' do
+ before { subject.active = false }
+
+ it { is_expected.not_to validate_presence_of(:webhook) }
+ end
+ end
+
+ describe "#execute" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:username) { 'slack_username' }
+ let(:channel) { 'slack_channel' }
+
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
+
+ before do
+ allow(chat_service).to receive_messages(
+ project: project,
+ project_id: project.id,
+ service_hook: true,
+ webhook: webhook_url
+ )
+
+ WebMock.stub_request(:post, webhook_url)
+
+ opts = {
+ title: 'Awesome issue',
+ description: 'please fix'
+ }
+
+ issue_service = Issues::CreateService.new(project, user, opts)
+ @issue = issue_service.execute
+ @issues_sample_data = issue_service.hook_data(@issue, 'open')
+
+ opts = {
+ title: 'Awesome merge_request',
+ description: 'please fix',
+ source_branch: 'feature',
+ target_branch: 'master'
+ }
+ merge_service = MergeRequests::CreateService.new(project,
+ user, opts)
+ @merge_request = merge_service.execute
+ @merge_sample_data = merge_service.hook_data(@merge_request,
+ 'open')
+
+ opts = {
+ title: "Awesome wiki_page",
+ content: "Some text describing some thing or another",
+ format: "md",
+ message: "user created page: Awesome wiki_page"
+ }
+
+ wiki_page_service = WikiPages::CreateService.new(project, user, opts)
+ @wiki_page = wiki_page_service.execute
+ @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create')
+ end
+
+ it "calls Slack/Mattermost API for push events" do
+ chat_service.execute(push_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+
+ it "calls Slack/Mattermost API for issue events" do
+ chat_service.execute(@issues_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+
+ it "calls Slack/Mattermost API for merge requests events" do
+ chat_service.execute(@merge_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+
+ it "calls Slack/Mattermost API for wiki page events" do
+ chat_service.execute(@wiki_page_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+
+ it 'uses the username as an option for slack when configured' do
+ allow(chat_service).to receive(:username).and_return(username)
+
+ expect(Slack::Notifier).to receive(:new).
+ with(webhook_url, username: username, channel: chat_service.default_channel).
+ and_return(
+ double(:slack_service).as_null_object
+ )
+
+ chat_service.execute(push_sample_data)
+ end
+
+ it 'uses the channel as an option when it is configured' do
+ allow(chat_service).to receive(:channel).and_return(channel)
+ expect(Slack::Notifier).to receive(:new).
+ with(webhook_url, channel: channel).
+ and_return(
+ double(:slack_service).as_null_object
+ )
+ chat_service.execute(push_sample_data)
+ end
+
+ context "event channels" do
+ it "uses the right channel for push event" do
+ chat_service.update_attributes(push_channel: "random")
+
+ expect(Slack::Notifier).to receive(:new).
+ with(webhook_url, channel: "random").
+ and_return(
+ double(:slack_service).as_null_object
+ )
+
+ chat_service.execute(push_sample_data)
+ end
+
+ it "uses the right channel for merge request event" do
+ chat_service.update_attributes(merge_request_channel: "random")
+
+ expect(Slack::Notifier).to receive(:new).
+ with(webhook_url, channel: "random").
+ and_return(
+ double(:slack_service).as_null_object
+ )
+
+ chat_service.execute(@merge_sample_data)
+ end
+
+ it "uses the right channel for issue event" do
+ chat_service.update_attributes(issue_channel: "random")
+
+ expect(Slack::Notifier).to receive(:new).
+ with(webhook_url, channel: "random").
+ and_return(
+ double(:slack_service).as_null_object
+ )
+
+ chat_service.execute(@issues_sample_data)
+ end
+
+ it "uses the right channel for wiki event" do
+ chat_service.update_attributes(wiki_page_channel: "random")
+
+ expect(Slack::Notifier).to receive(:new).
+ with(webhook_url, channel: "random").
+ and_return(
+ double(:slack_service).as_null_object
+ )
+
+ chat_service.execute(@wiki_page_sample_data)
+ end
+
+ context "note event" do
+ let(:issue_note) do
+ create(:note_on_issue, project: project, note: "issue note")
+ end
+
+ it "uses the right channel" do
+ chat_service.update_attributes(note_channel: "random")
+
+ note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
+
+ expect(Slack::Notifier).to receive(:new).
+ with(webhook_url, channel: "random").
+ and_return(
+ double(:slack_service).as_null_object
+ )
+
+ chat_service.execute(note_data)
+ end
+ end
+ end
+ end
+
+ describe "Note events" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, creator_id: user.id) }
+
+ before do
+ allow(chat_service).to receive_messages(
+ project: project,
+ project_id: project.id,
+ service_hook: true,
+ webhook: webhook_url
+ )
+
+ WebMock.stub_request(:post, webhook_url)
+ end
+
+ context 'when commit comment event executed' do
+ let(:commit_note) do
+ create(:note_on_commit, author: user,
+ project: project,
+ commit_id: project.repository.commit.id,
+ note: 'a comment on a commit')
+ end
+
+ it "calls Slack/Mattermost API for commit comment events" do
+ data = Gitlab::DataBuilder::Note.build(commit_note, user)
+ chat_service.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'when merge request comment event executed' do
+ let(:merge_request_note) do
+ create(:note_on_merge_request, project: project,
+ note: "merge request note")
+ end
+
+ it "calls Slack API for merge request comment events" do
+ data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
+ chat_service.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'when issue comment event executed' do
+ let(:issue_note) do
+ create(:note_on_issue, project: project, note: "issue note")
+ end
+
+ it "calls Slack API for issue comment events" do
+ data = Gitlab::DataBuilder::Note.build(issue_note, user)
+ chat_service.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'when snippet comment event executed' do
+ let(:snippet_note) do
+ create(:note_on_project_snippet, project: project,
+ note: "snippet note")
+ end
+
+ it "calls Slack API for snippet comment events" do
+ data = Gitlab::DataBuilder::Note.build(snippet_note, user)
+ chat_service.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+ end
+
+ describe 'Pipeline events' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project, status: status,
+ sha: project.commit.sha, ref: project.default_branch)
+ end
+
+ before do
+ allow(chat_service).to receive_messages(
+ project: project,
+ service_hook: true,
+ webhook: webhook_url
+ )
+ end
+
+ shared_examples 'call Slack/Mattermost API' do
+ before do
+ WebMock.stub_request(:post, webhook_url)
+ end
+
+ it 'calls Slack/Mattermost API for pipeline events' do
+ data = Gitlab::DataBuilder::Pipeline.build(pipeline)
+ chat_service.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'with failed pipeline' do
+ let(:status) { 'failed' }
+
+ it_behaves_like 'call Slack/Mattermost API'
+ end
+
+ context 'with succeeded pipeline' do
+ let(:status) { 'success' }
+
+ context 'with default to notify_only_broken_pipelines' do
+ it 'does not call Slack/Mattermost API for pipeline events' do
+ data = Gitlab::DataBuilder::Pipeline.build(pipeline)
+ result = chat_service.execute(data)
+
+ expect(result).to be_falsy
+ end
+ end
+
+ context 'with setting notify_only_broken_pipelines to false' do
+ before do
+ chat_service.notify_only_broken_pipelines = false
+ end
+
+ it_behaves_like 'call Slack/Mattermost API'
+ end
+ end
+ end
+end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 778e665500d..4cf81be3adc 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -23,6 +23,7 @@ module TestEnv
'binary-encoding' => '7b1cf43',
'gitattributes' => '5a62481',
'expand-collapse-diffs' => '4842455',
+ 'symlink-expand-diff' => '81e6355',
'expand-collapse-files' => '025db92',
'expand-collapse-lines' => '238e82d',
'video' => '8879059',
@@ -34,6 +35,7 @@ module TestEnv
'conflict-missing-side' => 'eb227b3',
'conflict-non-utf8' => 'd0a293c',
'conflict-too-large' => '39fa04f',
+ 'deleted-image-test' => '6c17798'
}
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
diff --git a/spec/support/upload_helpers.rb b/spec/support/upload_helpers.rb
new file mode 100644
index 00000000000..5eead80c935
--- /dev/null
+++ b/spec/support/upload_helpers.rb
@@ -0,0 +1,16 @@
+require 'fileutils'
+
+module UploadHelpers
+ extend self
+
+ def uploaded_image_temp_path
+ basename = 'banana_sample.gif'
+ orig_path = File.join(Rails.root, 'spec', 'fixtures', basename)
+ tmp_path = File.join(Rails.root, 'tmp', 'tests', basename)
+ # Because we use 'move_to_store' on all uploaders, we create a new
+ # tempfile on each call: the file we return here will be renamed in most
+ # cases.
+ FileUtils.copy(orig_path, tmp_path)
+ tmp_path
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 287d83344db..a9fea5f1e81 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -5,7 +5,7 @@ describe 'gitlab:app namespace rake task' do
let(:enable_registry) { true }
before :all do
- Rake.application.rake_require 'tasks/gitlab/task_helpers'
+ Rake.application.rake_require 'tasks/gitlab/helpers'
Rake.application.rake_require 'tasks/gitlab/backup'
Rake.application.rake_require 'tasks/gitlab/shell'
Rake.application.rake_require 'tasks/gitlab/db'
@@ -333,4 +333,35 @@ describe 'gitlab:app namespace rake task' do
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end
end
+
+ describe "Human Readable Backup Name" do
+ def tars_glob
+ Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar'))
+ end
+
+ before :all do
+ @origin_cd = Dir.pwd
+
+ reenable_backup_sub_tasks
+
+ FileUtils.rm tars_glob
+
+ # Redirect STDOUT and run the rake task
+ orig_stdout = $stdout
+ $stdout = StringIO.new
+ run_rake_task('gitlab:backup:create')
+ $stdout = orig_stdout
+
+ @backup_tar = tars_glob.first
+ end
+
+ after :all do
+ FileUtils.rm(@backup_tar)
+ Dir.chdir @origin_cd
+ end
+
+ it 'name has human readable time' do
+ expect(@backup_tar).to match(/\d+_\d{4}_\d{2}_\d{2}_gitlab_backup.tar$/)
+ end
+ end
end # gitlab:app namespace
diff --git a/spec/tasks/gitlab/check_rake_spec.rb b/spec/tasks/gitlab/check_rake_spec.rb
new file mode 100644
index 00000000000..538ff952bf4
--- /dev/null
+++ b/spec/tasks/gitlab/check_rake_spec.rb
@@ -0,0 +1,51 @@
+require 'rake_helper'
+
+describe 'gitlab:ldap:check rake task' do
+ include LdapHelpers
+
+ before do
+ Rake.application.rake_require 'tasks/gitlab/check'
+
+ stub_warn_user_is_not_gitlab
+ end
+
+ context 'when LDAP is not enabled' do
+ it 'does not attempt to bind or search for users' do
+ expect(Gitlab::LDAP::Config).not_to receive(:providers)
+ expect(Gitlab::LDAP::Adapter).not_to receive(:open)
+
+ run_rake_task('gitlab:ldap:check')
+ end
+ end
+
+ context 'when LDAP is enabled' do
+ let(:ldap) { double(:ldap) }
+ let(:adapter) { ldap_adapter('ldapmain', ldap) }
+
+ before do
+ allow(Gitlab::LDAP::Config)
+ .to receive_messages(
+ enabled?: true,
+ providers: ['ldapmain']
+ )
+ allow(Gitlab::LDAP::Adapter).to receive(:open).and_yield(adapter)
+ allow(adapter).to receive(:users).and_return([])
+ end
+
+ it 'attempts to bind using credentials' do
+ stub_ldap_config(has_auth?: true)
+
+ expect(ldap).to receive(:bind)
+
+ run_rake_task('gitlab:ldap:check')
+ end
+
+ it 'searches for 100 LDAP users' do
+ stub_ldap_config(uid: 'uid')
+
+ expect(adapter).to receive(:users).with('uid', '*', 100)
+
+ run_rake_task('gitlab:ldap:check')
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb
index 37feb5e6faf..80fc8c48fed 100644
--- a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb
+++ b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb
@@ -3,7 +3,7 @@ require 'rake'
describe 'gitlab:mail_google_schema_whitelisting rake task' do
before :all do
- Rake.application.rake_require "tasks/gitlab/task_helpers"
+ Rake.application.rake_require "tasks/gitlab/helpers"
Rake.application.rake_require "tasks/gitlab/mail_google_schema_whitelisting"
# empty task as env is already loaded
Rake::Task.define_task :environment
diff --git a/spec/tasks/gitlab/task_helpers_spec.rb b/spec/tasks/gitlab/task_helpers_spec.rb
new file mode 100644
index 00000000000..86e42d845ce
--- /dev/null
+++ b/spec/tasks/gitlab/task_helpers_spec.rb
@@ -0,0 +1,96 @@
+require 'spec_helper'
+require 'tasks/gitlab/task_helpers'
+
+class TestHelpersTest
+ include Gitlab::TaskHelpers
+end
+
+describe Gitlab::TaskHelpers do
+ subject { TestHelpersTest.new }
+
+ let(:repo) { 'https://gitlab.com/gitlab-org/gitlab-test.git' }
+ let(:clone_path) { Rails.root.join('tmp/tests/task_helpers_tests').to_s }
+ let(:tag) { 'v1.1.0' }
+
+ describe '#checkout_or_clone_tag' do
+ before do
+ allow(subject).to receive(:run_command!)
+ expect(subject).to receive(:reset_to_tag).with(tag, clone_path)
+ end
+
+ context 'target_dir does not exist' do
+ it 'clones the repo, retrieve the tag from origin, and checkout the tag' do
+ expect(subject).to receive(:clone_repo).with(repo, clone_path)
+
+ subject.checkout_or_clone_tag(tag: tag, repo: repo, target_dir: clone_path)
+ end
+ end
+
+ context 'target_dir exists' do
+ before do
+ expect(Dir).to receive(:exist?).and_return(true)
+ end
+
+ it 'fetch and checkout the tag' do
+ expect(subject).to receive(:checkout_tag).with(tag, clone_path)
+
+ subject.checkout_or_clone_tag(tag: tag, repo: repo, target_dir: clone_path)
+ end
+ end
+ end
+
+ describe '#clone_repo' do
+ it 'clones the repo in the target dir' do
+ expect(subject).
+ to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} clone -- #{repo} #{clone_path}])
+
+ subject.clone_repo(repo, clone_path)
+ end
+ end
+
+ describe '#checkout_tag' do
+ it 'clones the repo in the target dir' do
+ expect(subject).
+ to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch --tags --quiet])
+ expect(subject).
+ to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} checkout --quiet #{tag}])
+
+ subject.checkout_tag(tag, clone_path)
+ end
+ end
+
+ describe '#reset_to_tag' do
+ let(:tag) { 'v1.1.0' }
+ before do
+ expect(subject).
+ to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} reset --hard #{tag}])
+ end
+
+ context 'when the tag is not checked out locally' do
+ before do
+ expect(subject).
+ to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- #{tag}]).and_raise(Gitlab::TaskFailedError)
+ end
+
+ it 'fetch origin, ensure the tag exists, and resets --hard to the given tag' do
+ expect(subject).
+ to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch origin])
+ expect(subject).
+ to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- origin/#{tag}]).and_return(tag)
+
+ subject.reset_to_tag(tag, clone_path)
+ end
+ end
+
+ context 'when the tag is checked out locally' do
+ before do
+ expect(subject).
+ to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- #{tag}]).and_return(tag)
+ end
+
+ it 'resets --hard to the given tag' do
+ subject.reset_to_tag(tag, clone_path)
+ end
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/users_rake_spec.rb b/spec/tasks/gitlab/users_rake_spec.rb
index e6ebef82b78..972670e7f91 100644
--- a/spec/tasks/gitlab/users_rake_spec.rb
+++ b/spec/tasks/gitlab/users_rake_spec.rb
@@ -5,7 +5,7 @@ describe 'gitlab:users namespace rake task' do
let(:enable_registry) { true }
before :all do
- Rake.application.rake_require 'tasks/gitlab/task_helpers'
+ Rake.application.rake_require 'tasks/gitlab/helpers'
Rake.application.rake_require 'tasks/gitlab/users'
# empty task as env is already loaded
diff --git a/spec/tasks/gitlab/workhorse_rake_spec.rb b/spec/tasks/gitlab/workhorse_rake_spec.rb
new file mode 100644
index 00000000000..6de66c3cf07
--- /dev/null
+++ b/spec/tasks/gitlab/workhorse_rake_spec.rb
@@ -0,0 +1,81 @@
+require 'rake_helper'
+
+describe 'gitlab:workhorse namespace rake task' do
+ before :all do
+ Rake.application.rake_require 'tasks/gitlab/workhorse'
+ end
+
+ describe 'install' do
+ let(:repo) { 'https://gitlab.com/gitlab-org/gitlab-workhorse.git' }
+ let(:clone_path) { Rails.root.join('tmp/tests/gitlab-workhorse').to_s }
+ let(:tag) { "v#{File.read(Rails.root.join(Gitlab::Workhorse::VERSION_FILE)).chomp}" }
+ before do
+ allow(ENV).to receive(:[])
+ end
+
+ context 'no dir given' do
+ it 'aborts and display a help message' do
+ # avoid writing task output to spec progress
+ allow($stderr).to receive :write
+ expect { run_rake_task('gitlab:workhorse:install') }.to raise_error /Please specify the directory where you want to install gitlab-workhorse/
+ end
+ end
+
+ context 'when an underlying Git command fail' do
+ it 'aborts and display a help message' do
+ expect_any_instance_of(Object).
+ to receive(:checkout_or_clone_tag).and_raise 'Git error'
+
+ expect { run_rake_task('gitlab:workhorse:install', clone_path) }.to raise_error 'Git error'
+ end
+ end
+
+ describe 'checkout or clone' do
+ before do
+ expect(Dir).to receive(:chdir).with(clone_path)
+ end
+
+ it 'calls checkout_or_clone_tag with the right arguments' do
+ expect_any_instance_of(Object).
+ to receive(:checkout_or_clone_tag).with(tag: tag, repo: repo, target_dir: clone_path)
+
+ run_rake_task('gitlab:workhorse:install', clone_path)
+ end
+ end
+
+ describe 'gmake/make' do
+ before do
+ FileUtils.mkdir_p(clone_path)
+ expect(Dir).to receive(:chdir).with(clone_path).and_call_original
+ end
+
+ context 'gmake is available' do
+ before do
+ expect_any_instance_of(Object).to receive(:checkout_or_clone_tag)
+ allow_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true)
+ end
+
+ it 'calls gmake in the gitlab-workhorse directory' do
+ expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['/usr/bin/gmake', 0])
+ expect_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true)
+
+ run_rake_task('gitlab:workhorse:install', clone_path)
+ end
+ end
+
+ context 'gmake is not available' do
+ before do
+ expect_any_instance_of(Object).to receive(:checkout_or_clone_tag)
+ allow_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true)
+ end
+
+ it 'calls make in the gitlab-workhorse directory' do
+ expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['', 42])
+ expect_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true)
+
+ run_rake_task('gitlab:workhorse:install', clone_path)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb
new file mode 100644
index 00000000000..6098be5cd45
--- /dev/null
+++ b/spec/uploaders/attachment_uploader_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe AttachmentUploader do
+ let(:issue) { build(:issue) }
+ subject { described_class.new(issue) }
+
+ describe '#move_to_cache' do
+ it 'is true' do
+ expect(subject.move_to_cache).to eq(true)
+ end
+ end
+
+ describe '#move_to_store' do
+ it 'is true' do
+ expect(subject.move_to_store).to eq(true)
+ end
+ end
+end
diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb
new file mode 100644
index 00000000000..1f0e8732587
--- /dev/null
+++ b/spec/uploaders/avatar_uploader_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe AvatarUploader do
+ let(:user) { build(:user) }
+ subject { described_class.new(user) }
+
+ describe '#move_to_cache' do
+ it 'is true' do
+ expect(subject.move_to_cache).to eq(true)
+ end
+ end
+
+ describe '#move_to_store' do
+ it 'is true' do
+ expect(subject.move_to_store).to eq(true)
+ end
+ end
+end
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index e8300abed5d..bc86adfef68 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -42,4 +42,16 @@ describe FileUploader do
expect(@uploader.image_or_video?).to be false
end
end
+
+ describe '#move_to_cache' do
+ it 'is true' do
+ expect(@uploader.move_to_cache).to eq(true)
+ end
+ end
+
+ describe '#move_to_store' do
+ it 'is true' do
+ expect(@uploader.move_to_store).to eq(true)
+ end
+ end
end
diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb
index 3fddfb3b62f..8020faa1f9c 100644
--- a/spec/views/layouts/_head.html.haml_spec.rb
+++ b/spec/views/layouts/_head.html.haml_spec.rb
@@ -1,10 +1,6 @@
require 'spec_helper'
describe 'layouts/_head' do
- before do
- stub_template 'layouts/_user_styles.html.haml' => ''
- end
-
it 'escapes HTML-safe strings in page_title' do
stub_helper_with_safe_string(:page_title)
diff --git a/spec/views/projects/builds/_build.html.haml_spec.rb b/spec/views/projects/builds/_build.html.haml_spec.rb
new file mode 100644
index 00000000000..e141a117731
--- /dev/null
+++ b/spec/views/projects/builds/_build.html.haml_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe 'projects/ci/builds/_build' do
+ include Devise::Test::ControllerHelpers
+
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_empty_pipeline, id: 1337, project: project, sha: project.commit.id) }
+ let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', stage_idx: 1, name: 'rspec 0:2', status: :pending) }
+
+ before do
+ controller.prepend_view_path('app/views/projects')
+ allow(view).to receive(:can?).and_return(true)
+ end
+
+ it 'won\'t include a column with a link to its pipeline by default' do
+ render partial: 'projects/ci/builds/build', locals: { build: build }
+
+ expect(rendered).not_to have_link('#1337')
+ expect(rendered).not_to have_text('#1337 by API')
+ end
+
+ it 'can include a column with a link to its pipeline' do
+ render partial: 'projects/ci/builds/build', locals: { build: build, pipeline_link: true }
+
+ expect(rendered).to have_link('#1337')
+ expect(rendered).to have_text('#1337 by API')
+ end
+end
diff --git a/spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb b/spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb
new file mode 100644
index 00000000000..49b20e5b36b
--- /dev/null
+++ b/spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe 'projects/generic_commit_statuses/_generic_commit_status.html.haml' do
+ include Devise::Test::ControllerHelpers
+
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_empty_pipeline, id: 1337, project: project, sha: project.commit.id) }
+ let(:generic_commit_status) { create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3) }
+
+ before do
+ controller.prepend_view_path('app/views/projects')
+ allow(view).to receive(:can?).and_return(true)
+ end
+
+ it 'won\'t include a column with a link to its pipeline by default' do
+ render partial: 'projects/generic_commit_statuses/generic_commit_status', locals: { generic_commit_status: generic_commit_status }
+
+ expect(rendered).not_to have_link('#1337')
+ expect(rendered).not_to have_text('#1337 by API')
+ end
+
+ it 'can include a column with a link to its pipeline' do
+ render partial: 'projects/generic_commit_statuses/generic_commit_status', locals: { generic_commit_status: generic_commit_status, pipeline_link: true }
+
+ expect(rendered).to have_link('#1337')
+ expect(rendered).to have_text('#1337 by API')
+ end
+end
diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb
index da43622d3f9..745d0c745bd 100644
--- a/spec/views/projects/builds/show.html.haml_spec.rb
+++ b/spec/views/projects/builds/show.html.haml_spec.rb
@@ -1,14 +1,12 @@
require 'spec_helper'
-describe 'projects/builds/show' do
- include Devise::Test::ControllerHelpers
-
+describe 'projects/builds/show', :view do
let(:project) { create(:project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
let(:pipeline) do
- create(:ci_pipeline, project: project,
- sha: project.commit.id)
+ create(:ci_pipeline, project: project, sha: project.commit.id)
end
- let(:build) { create(:ci_build, pipeline: pipeline) }
before do
assign(:build, build)
@@ -17,6 +15,161 @@ describe 'projects/builds/show' do
allow(view).to receive(:can?).and_return(true)
end
+ describe 'environment info in build view' do
+ context 'build with latest deployment' do
+ let(:build) do
+ create(:ci_build, :success, environment: 'staging')
+ end
+
+ before do
+ create(:environment, name: 'staging')
+ create(:deployment, deployable: build)
+ end
+
+ it 'shows deployment message' do
+ expected_text = 'This build is the most recent deployment'
+ render
+
+ expect(rendered).to have_css(
+ '.environment-information', text: expected_text)
+ end
+ end
+
+ context 'build with outdated deployment' do
+ let(:build) do
+ create(:ci_build, :success, environment: 'staging', pipeline: pipeline)
+ end
+
+ let(:second_build) do
+ create(:ci_build, :success, environment: 'staging', pipeline: pipeline)
+ end
+
+ let(:environment) do
+ create(:environment, name: 'staging', project: project)
+ end
+
+ let!(:first_deployment) do
+ create(:deployment, environment: environment, deployable: build)
+ end
+
+ let!(:second_deployment) do
+ create(:deployment, environment: environment, deployable: second_build)
+ end
+
+ it 'shows deployment message' do
+ expected_text = 'This build is an out-of-date deployment ' \
+ "to staging.\nView the most recent deployment ##{second_deployment.iid}."
+ render
+
+ expect(rendered).to have_css('.environment-information', text: expected_text)
+ end
+ end
+
+ context 'build failed to deploy' do
+ let(:build) do
+ create(:ci_build, :failed, environment: 'staging', pipeline: pipeline)
+ end
+
+ let!(:environment) do
+ create(:environment, name: 'staging', project: project)
+ end
+
+ it 'shows deployment message' do
+ expected_text = 'The deployment of this build to staging did not succeed.'
+ render
+
+ expect(rendered).to have_css(
+ '.environment-information', text: expected_text)
+ end
+ end
+
+ context 'build will deploy' do
+ let(:build) do
+ create(:ci_build, :running, environment: 'staging', pipeline: pipeline)
+ end
+
+ context 'when environment exists' do
+ let!(:environment) do
+ create(:environment, name: 'staging', project: project)
+ end
+
+ it 'shows deployment message' do
+ expected_text = 'This build is creating a deployment to staging'
+ render
+
+ expect(rendered).to have_css(
+ '.environment-information', text: expected_text)
+ end
+
+ context 'when it has deployment' do
+ let!(:deployment) do
+ create(:deployment, environment: environment)
+ end
+
+ it 'shows that deployment will be overwritten' do
+ expected_text = 'This build is creating a deployment to staging'
+ render
+
+ expect(rendered).to have_css(
+ '.environment-information', text: expected_text)
+ expect(rendered).to have_css(
+ '.environment-information', text: 'latest deployment')
+ end
+ end
+ end
+
+ context 'when environment does not exist' do
+ it 'shows deployment message' do
+ expected_text = 'This build is creating a deployment to staging'
+ render
+
+ expect(rendered).to have_css(
+ '.environment-information', text: expected_text)
+ expect(rendered).not_to have_css(
+ '.environment-information', text: 'latest deployment')
+ end
+ end
+ end
+
+ context 'build that failed to deploy and environment has not been created' do
+ let(:build) do
+ create(:ci_build, :failed, environment: 'staging', pipeline: pipeline)
+ end
+
+ let!(:environment) do
+ create(:environment, name: 'staging', project: project)
+ end
+
+ it 'shows deployment message' do
+ expected_text = 'The deployment of this build to staging did not succeed'
+ render
+
+ expect(rendered).to have_css(
+ '.environment-information', text: expected_text)
+ end
+ end
+
+ context 'build that will deploy and environment has not been created' do
+ let(:build) do
+ create(:ci_build, :running, environment: 'staging', pipeline: pipeline)
+ end
+
+ let!(:environment) do
+ create(:environment, name: 'staging', project: project)
+ end
+
+ it 'shows deployment message' do
+ expected_text = 'This build is creating a deployment to staging'
+ render
+
+ expect(rendered).to have_css(
+ '.environment-information', text: expected_text)
+ expect(rendered).not_to have_css(
+ '.environment-information', text: 'latest deployment')
+ end
+ end
+ end
+
context 'when build is running' do
before do
build.run!
diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
index 16bf0698c4b..e741e3cf9b6 100644
--- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb
+++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
@@ -13,7 +13,7 @@ describe 'projects/commit/_commit_box.html.haml' do
it 'shows the commit SHA' do
render
- expect(rendered).to have_text("Commit #{Commit.truncate_sha(project.commit.sha)}")
+ expect(rendered).to have_text("#{Commit.truncate_sha(project.commit.sha)}")
end
it 'shows the last pipeline that ran for the commit' do
diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb
new file mode 100644
index 00000000000..d2575702ecc
--- /dev/null
+++ b/spec/views/projects/edit.html.haml_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe 'projects/edit' do
+ include Devise::Test::ControllerHelpers
+
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:admin) }
+
+ before do
+ assign(:project, project)
+
+ allow(controller).to receive(:current_user).and_return(user)
+ allow(view).to receive_messages(current_user: user, can?: true)
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
+ context 'LFS enabled setting' do
+ it 'displays the correct elements' do
+ render
+ expect(rendered).to have_select('project_lfs_enabled')
+ expect(rendered).to have_content('Git Large File Storage')
+ end
+ end
+end
diff --git a/spec/views/projects/merge_requests/_new_submit.html.haml_spec.rb b/spec/views/projects/merge_requests/_new_submit.html.haml_spec.rb
new file mode 100644
index 00000000000..4f698a34ab5
--- /dev/null
+++ b/spec/views/projects/merge_requests/_new_submit.html.haml_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe 'projects/merge_requests/_new_submit.html.haml', :view do
+ let(:merge_request) { create(:merge_request) }
+ let!(:pipeline) { create(:ci_empty_pipeline) }
+
+ before do
+ controller.prepend_view_path('app/views/projects')
+
+ assign(:merge_request, merge_request)
+ assign(:commits, merge_request.commits)
+ assign(:project, merge_request.target_project)
+
+ allow(view).to receive(:can?).and_return(true)
+ allow(view).to receive(:url_for).and_return('#')
+ allow(view).to receive(:current_user).and_return(merge_request.author)
+ end
+
+ context 'when there are pipelines for merge request but no pipeline for last commit' do
+ before do
+ assign(:pipelines, Ci::Pipeline.all)
+ assign(:pipeline, nil)
+ end
+
+ it 'shows <<Pipelines>> tab and hides <<Builds>> tab' do
+ render
+ expect(rendered).to have_text('Pipelines 1')
+ expect(rendered).not_to have_text('Builds')
+ end
+ end
+end
diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb
index bf027499c94..a066ea078e6 100644
--- a/spec/views/projects/pipelines/show.html.haml_spec.rb
+++ b/spec/views/projects/pipelines/show.html.haml_spec.rb
@@ -28,7 +28,7 @@ describe 'projects/pipelines/show' do
it 'shows a graph with grouped stages' do
render
- expect(rendered).to have_css('.pipeline-graph')
+ expect(rendered).to have_css('.js-pipeline-graph')
expect(rendered).to have_css('.grouped-pipeline-dropdown')
# stages
diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb
new file mode 100644
index 00000000000..95e2458da35
--- /dev/null
+++ b/spec/workers/authorized_projects_worker_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe AuthorizedProjectsWorker do
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ it "refreshes user's authorized projects" do
+ user = create(:user)
+
+ expect(worker).to receive(:refresh).with(an_instance_of(User))
+
+ worker.perform(user.id)
+ end
+
+ context "when the user is not found" do
+ it "does nothing" do
+ expect(worker).not_to receive(:refresh)
+
+ described_class.new.perform(-1)
+ end
+ end
+ end
+
+ describe '#refresh', redis: true do
+ it 'refreshes the authorized projects of the user' do
+ user = create(:user)
+
+ expect(user).to receive(:refresh_authorized_projects)
+
+ worker.refresh(user)
+ end
+ end
+end
diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb
index 788b92c1b84..542e674c150 100644
--- a/spec/workers/build_email_worker_spec.rb
+++ b/spec/workers/build_email_worker_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe BuildEmailWorker do
+ include EmailHelpers
include RepoHelpers
let(:build) { create(:ci_build) }
@@ -24,7 +25,7 @@ describe BuildEmailWorker do
end
it "gracefully handles an input SMTP error" do
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
allow(Notify).to receive(:build_success_email).and_raise(Net::SMTPFatalError)
subject.perform(build.id, [user.email], data.stringify_keys)
diff --git a/spec/workers/delete_merged_branches_worker_spec.rb b/spec/workers/delete_merged_branches_worker_spec.rb
new file mode 100644
index 00000000000..d9497bd486c
--- /dev/null
+++ b/spec/workers/delete_merged_branches_worker_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe DeleteMergedBranchesWorker do
+ subject(:worker) { described_class.new }
+
+ let(:project) { create(:project) }
+
+ describe "#perform" do
+ it "calls DeleteMergedBranchesService" do
+ expect_any_instance_of(DeleteMergedBranchesService).to receive(:execute).and_return(true)
+
+ worker.perform(project.id, project.owner.id)
+ end
+
+ it "returns false when project was not found" do
+ expect(worker.perform('unknown', project.owner.id)).to be_falsy
+ end
+ end
+end
diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index 036d037f3f9..f27e413f7b8 100644
--- a/spec/workers/emails_on_push_worker_spec.rb
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe EmailsOnPushWorker do
include RepoHelpers
+ include EmailHelpers
include EmailSpec::Matchers
let(:project) { create(:project) }
@@ -87,7 +88,7 @@ describe EmailsOnPushWorker do
context "when there is an SMTP error" do
before do
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError)
allow(subject).to receive_message_chain(:logger, :info)
perform
@@ -112,7 +113,7 @@ describe EmailsOnPushWorker do
original.call(Mail.new(mail.encoded))
end
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
end
it "sends the mail to each of the recipients" do
diff --git a/spec/workers/new_note_worker_spec.rb b/spec/workers/new_note_worker_spec.rb
new file mode 100644
index 00000000000..8fdbb35afd0
--- /dev/null
+++ b/spec/workers/new_note_worker_spec.rb
@@ -0,0 +1,49 @@
+require "spec_helper"
+
+describe NewNoteWorker do
+ context 'when Note found' do
+ let(:note) { create(:note) }
+
+ it "calls NotificationService#new_note" do
+ expect_any_instance_of(NotificationService).to receive(:new_note).with(note)
+
+ described_class.new.perform(note.id)
+ end
+
+ it "calls Notes::PostProcessService#execute" do
+ notes_post_process_service = double(Notes::PostProcessService)
+ allow(Notes::PostProcessService).to receive(:new).with(note) { notes_post_process_service }
+
+ expect(notes_post_process_service).to receive(:execute)
+
+ described_class.new.perform(note.id)
+ end
+ end
+
+ context 'when Note not found' do
+ let(:unexistent_note_id) { 999 }
+
+ it 'logs NewNoteWorker process skipping' do
+ expect(Rails.logger).to receive(:error).
+ with("NewNoteWorker: couldn't find note with ID=999, skipping job")
+
+ described_class.new.perform(unexistent_note_id)
+ end
+
+ it 'does not raise errors' do
+ expect { described_class.new.perform(unexistent_note_id) }.not_to raise_error
+ end
+
+ it "does not call NotificationService#new_note" do
+ expect_any_instance_of(NotificationService).not_to receive(:new_note)
+
+ described_class.new.perform(unexistent_note_id)
+ end
+
+ it "does not call Notes::PostProcessService#execute" do
+ expect_any_instance_of(Notes::PostProcessService).not_to receive(:execute)
+
+ described_class.new.perform(unexistent_note_id)
+ end
+ end
+end
diff --git a/spec/workers/pipeline_metrics_worker_spec.rb b/spec/workers/pipeline_metrics_worker_spec.rb
index 2c9e7c2cd02..2d47d93acec 100644
--- a/spec/workers/pipeline_metrics_worker_spec.rb
+++ b/spec/workers/pipeline_metrics_worker_spec.rb
@@ -15,32 +15,36 @@ describe PipelineMetricsWorker do
end
describe '#perform' do
- subject { described_class.new.perform(pipeline.id) }
+ before do
+ described_class.new.perform(pipeline.id)
+ end
context 'when pipeline is running' do
let(:status) { 'running' }
it 'records the build start time' do
- subject
-
expect(merge_request.reload.metrics.latest_build_started_at).to be_like_time(pipeline.started_at)
end
it 'clears the build end time' do
- subject
-
expect(merge_request.reload.metrics.latest_build_finished_at).to be_nil
end
+
+ it 'records the pipeline' do
+ expect(merge_request.reload.metrics.pipeline).to eq(pipeline)
+ end
end
context 'when pipeline succeeded' do
let(:status) { 'success' }
it 'records the build end time' do
- subject
-
expect(merge_request.reload.metrics.latest_build_finished_at).to be_like_time(pipeline.finished_at)
end
+
+ it 'records the pipeline' do
+ expect(merge_request.reload.metrics.pipeline).to eq(pipeline)
+ end
end
end
end
diff --git a/spec/workers/pipeline_notification_worker_spec.rb b/spec/workers/pipeline_notification_worker_spec.rb
new file mode 100644
index 00000000000..603ae52ed1e
--- /dev/null
+++ b/spec/workers/pipeline_notification_worker_spec.rb
@@ -0,0 +1,133 @@
+require 'spec_helper'
+
+describe PipelineNotificationWorker do
+ include EmailHelpers
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: project.commit('master').sha,
+ user: pusher,
+ status: status)
+ end
+
+ let(:project) { create(:project, public_builds: false) }
+ let(:user) { create(:user) }
+ let(:pusher) { user }
+ let(:watcher) { pusher }
+
+ describe '#execute' do
+ before do
+ reset_delivered_emails!
+ pipeline.project.team << [pusher, Gitlab::Access::DEVELOPER]
+ end
+
+ context 'when watcher has developer access' do
+ before do
+ pipeline.project.team << [watcher, Gitlab::Access::DEVELOPER]
+ end
+
+ shared_examples 'sending emails' do
+ it 'sends emails' do
+ perform_enqueued_jobs do
+ subject.perform(pipeline.id)
+ end
+
+ emails = ActionMailer::Base.deliveries
+ actual = emails.flat_map(&:bcc).sort
+ expected_receivers = receivers.map(&:email).uniq.sort
+
+ expect(actual).to eq(expected_receivers)
+ expect(emails.size).to eq(1)
+ expect(emails.last.subject).to include(email_subject)
+ end
+ end
+
+ context 'with success pipeline' do
+ let(:status) { 'success' }
+ let(:email_subject) { "Pipeline ##{pipeline.id} has succeeded" }
+ let(:receivers) { [pusher, watcher] }
+
+ it_behaves_like 'sending emails'
+
+ context 'with pipeline from someone else' do
+ let(:pusher) { create(:user) }
+ let(:watcher) { user }
+
+ context 'with success pipeline notification on' do
+ before do
+ watcher.global_notification_setting.
+ update(level: 'custom', success_pipeline: true)
+ end
+
+ it_behaves_like 'sending emails'
+ end
+
+ context 'with success pipeline notification off' do
+ let(:receivers) { [pusher] }
+
+ before do
+ watcher.global_notification_setting.
+ update(level: 'custom', success_pipeline: false)
+ end
+
+ it_behaves_like 'sending emails'
+ end
+ end
+
+ context 'with failed pipeline' do
+ let(:status) { 'failed' }
+ let(:email_subject) { "Pipeline ##{pipeline.id} has failed" }
+
+ it_behaves_like 'sending emails'
+
+ context 'with pipeline from someone else' do
+ let(:pusher) { create(:user) }
+ let(:watcher) { user }
+
+ context 'with failed pipeline notification on' do
+ before do
+ watcher.global_notification_setting.
+ update(level: 'custom', failed_pipeline: true)
+ end
+
+ it_behaves_like 'sending emails'
+ end
+
+ context 'with failed pipeline notification off' do
+ let(:receivers) { [pusher] }
+
+ before do
+ watcher.global_notification_setting.
+ update(level: 'custom', failed_pipeline: false)
+ end
+
+ it_behaves_like 'sending emails'
+ end
+ end
+ end
+ end
+ end
+
+ context 'when watcher has no read_build access' do
+ let(:status) { 'failed' }
+ let(:email_subject) { "Pipeline ##{pipeline.id} has failed" }
+ let(:watcher) { create(:user) }
+
+ before do
+ pipeline.project.team << [watcher, Gitlab::Access::GUEST]
+
+ watcher.global_notification_setting.
+ update(level: 'custom', failed_pipeline: true)
+
+ perform_enqueued_jobs do
+ subject.perform(pipeline.id)
+ end
+ end
+
+ it 'does not send emails' do
+ should_only_email(pusher, kind: :bcc)
+ end
+ end
+ end
+end
diff --git a/spec/workers/pipeline_success_worker_spec.rb b/spec/workers/pipeline_success_worker_spec.rb
index 5e31cc2c8e7..d1c84adda6f 100644
--- a/spec/workers/pipeline_success_worker_spec.rb
+++ b/spec/workers/pipeline_success_worker_spec.rb
@@ -7,7 +7,7 @@ describe PipelineSuccessWorker do
it 'performs "merge when pipeline succeeds"' do
expect_any_instance_of(
- MergeRequests::MergeWhenBuildSucceedsService
+ MergeRequests::MergeWhenPipelineSucceedsService
).to receive(:trigger)
described_class.new.perform(pipeline.id)
diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb
index 3e4fee42240..75c7fc1efd2 100644
--- a/spec/workers/process_commit_worker_spec.rb
+++ b/spec/workers/process_commit_worker_spec.rb
@@ -11,31 +11,25 @@ describe ProcessCommitWorker do
it 'does not process the commit when the project does not exist' do
expect(worker).not_to receive(:close_issues)
- worker.perform(-1, user.id, commit.id)
+ worker.perform(-1, user.id, commit.to_hash)
end
it 'does not process the commit when the user does not exist' do
expect(worker).not_to receive(:close_issues)
- worker.perform(project.id, -1, commit.id)
- end
-
- it 'does not process the commit when the commit no longer exists' do
- expect(worker).not_to receive(:close_issues)
-
- worker.perform(project.id, user.id, 'this-should-does-not-exist')
+ worker.perform(project.id, -1, commit.to_hash)
end
it 'processes the commit message' do
expect(worker).to receive(:process_commit_message).and_call_original
- worker.perform(project.id, user.id, commit.id)
+ worker.perform(project.id, user.id, commit.to_hash)
end
it 'updates the issue metrics' do
expect(worker).to receive(:update_issue_metrics).and_call_original
- worker.perform(project.id, user.id, commit.id)
+ worker.perform(project.id, user.id, commit.to_hash)
end
end
@@ -106,4 +100,19 @@ describe ProcessCommitWorker do
expect(metric.first_mentioned_in_commit_at).to eq(commit.committed_date)
end
end
+
+ describe '#build_commit' do
+ it 'returns a Commit' do
+ commit = worker.build_commit(project, id: '123')
+
+ expect(commit).to be_an_instance_of(Commit)
+ end
+
+ it 'parses date strings into Time instances' do
+ commit = worker.
+ build_commit(project, id: '123', authored_date: Time.now.to_s)
+
+ expect(commit.authored_date).to be_an_instance_of(Time)
+ end
+ end
end
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index bfa8c0ff2c6..855c28b584e 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -2,62 +2,78 @@ require 'spec_helper'
describe ProjectCacheWorker do
let(:project) { create(:project) }
+ let(:worker) { described_class.new }
- subject { described_class.new }
-
- describe '.perform_async' do
- it 'schedules the job when no lease exists' do
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:exists?).
- and_return(false)
+ describe '#perform' do
+ before do
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).
+ and_return(true)
+ end
- expect_any_instance_of(described_class).to receive(:perform)
+ context 'with a non-existing project' do
+ it 'does nothing' do
+ expect(worker).not_to receive(:update_repository_size)
- described_class.perform_async(project.id)
+ worker.perform(-1)
+ end
end
- it 'does not schedule the job when a lease exists' do
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:exists?).
- and_return(true)
+ context 'with an existing project without a repository' do
+ it 'does nothing' do
+ allow_any_instance_of(Repository).to receive(:exists?).and_return(false)
- expect_any_instance_of(described_class).not_to receive(:perform)
+ expect(worker).not_to receive(:update_repository_size)
- described_class.perform_async(project.id)
+ worker.perform(project.id)
+ end
end
- end
- describe '#perform' do
- context 'when an exclusive lease can be obtained' do
- before do
- allow(subject).to receive(:try_obtain_lease_for).with(project.id).
- and_return(true)
- end
+ context 'with an existing project' do
+ it 'updates the repository size' do
+ expect(worker).to receive(:update_repository_size).and_call_original
- it 'updates project cache data' do
- expect_any_instance_of(Repository).to receive(:size)
- expect_any_instance_of(Repository).to receive(:commit_count)
+ worker.perform(project.id)
+ end
- expect_any_instance_of(Project).to receive(:update_repository_size)
- expect_any_instance_of(Project).to receive(:update_commit_count)
+ it 'updates the commit count' do
+ expect_any_instance_of(Project).to receive(:update_commit_count).
+ and_call_original
- subject.perform(project.id)
+ worker.perform(project.id)
end
- it 'handles missing repository data' do
- expect_any_instance_of(Repository).to receive(:exists?).and_return(false)
- expect_any_instance_of(Repository).not_to receive(:size)
+ it 'refreshes the method caches' do
+ expect_any_instance_of(Repository).to receive(:refresh_method_caches).
+ with(%i(readme)).
+ and_call_original
- subject.perform(project.id)
+ worker.perform(project.id, %i(readme))
end
end
+ end
- context 'when an exclusive lease can not be obtained' do
- it 'does nothing' do
- allow(subject).to receive(:try_obtain_lease_for).with(project.id).
+ describe '#update_repository_size' do
+ context 'when a lease could not be obtained' do
+ it 'does not update the repository size' do
+ allow(worker).to receive(:try_obtain_lease_for).
+ with(project.id, :update_repository_size).
and_return(false)
- expect(subject).not_to receive(:update_caches)
+ expect(project).not_to receive(:update_repository_size)
+
+ worker.update_repository_size(project)
+ end
+ end
+
+ context 'when a lease could be obtained' do
+ it 'updates the repository size' do
+ allow(worker).to receive(:try_obtain_lease_for).
+ with(project.id, :update_repository_size).
+ and_return(true)
+
+ expect(project).to receive(:update_repository_size).and_call_original
- subject.perform(project.id)
+ worker.update_repository_size(project)
end
end
end
diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.eot b/vendor/assets/fonts/KaTeX_AMS-Regular.eot
new file mode 100644
index 00000000000..784276a3cbf
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_AMS-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.ttf b/vendor/assets/fonts/KaTeX_AMS-Regular.ttf
new file mode 100644
index 00000000000..6f1e0be2028
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_AMS-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.woff b/vendor/assets/fonts/KaTeX_AMS-Regular.woff
new file mode 100644
index 00000000000..4dded4733b3
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_AMS-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.woff2 b/vendor/assets/fonts/KaTeX_AMS-Regular.woff2
new file mode 100644
index 00000000000..ea81079c4e2
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_AMS-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.eot b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.eot
new file mode 100644
index 00000000000..1a0db0c568e
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttf b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttf
new file mode 100644
index 00000000000..b94907dad11
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff
new file mode 100644
index 00000000000..799fa8122ca
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2 b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2
new file mode 100644
index 00000000000..73bb5422878
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.eot b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.eot
new file mode 100644
index 00000000000..6cc83d0922c
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttf b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttf
new file mode 100644
index 00000000000..cf51e2021e4
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff
new file mode 100644
index 00000000000..f5e5c623577
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2 b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2
new file mode 100644
index 00000000000..dd76d3488d5
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.eot b/vendor/assets/fonts/KaTeX_Fraktur-Bold.eot
new file mode 100644
index 00000000000..1960b106656
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Fraktur-Bold.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.ttf b/vendor/assets/fonts/KaTeX_Fraktur-Bold.ttf
new file mode 100644
index 00000000000..7b0790f1ae8
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Fraktur-Bold.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff
new file mode 100644
index 00000000000..dc325713291
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2 b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2
new file mode 100644
index 00000000000..fdc429227ad
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.eot b/vendor/assets/fonts/KaTeX_Fraktur-Regular.eot
new file mode 100644
index 00000000000..e4e73796aea
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Fraktur-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.ttf b/vendor/assets/fonts/KaTeX_Fraktur-Regular.ttf
new file mode 100644
index 00000000000..063bc0263eb
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Fraktur-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff
new file mode 100644
index 00000000000..c4b18d863f3
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2 b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2
new file mode 100644
index 00000000000..4318d938e26
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.eot b/vendor/assets/fonts/KaTeX_Main-Bold.eot
new file mode 100644
index 00000000000..80fbd022363
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Bold.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.ttf b/vendor/assets/fonts/KaTeX_Main-Bold.ttf
new file mode 100644
index 00000000000..8e10722afae
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Bold.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.woff b/vendor/assets/fonts/KaTeX_Main-Bold.woff
new file mode 100644
index 00000000000..43b361a6005
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Bold.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.woff2 b/vendor/assets/fonts/KaTeX_Main-Bold.woff2
new file mode 100644
index 00000000000..af57a96c148
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Bold.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.eot b/vendor/assets/fonts/KaTeX_Main-Italic.eot
new file mode 100644
index 00000000000..fc770166b5e
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Italic.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.ttf b/vendor/assets/fonts/KaTeX_Main-Italic.ttf
new file mode 100644
index 00000000000..d124495d7b6
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Italic.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.woff b/vendor/assets/fonts/KaTeX_Main-Italic.woff
new file mode 100644
index 00000000000..e623236bc44
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Italic.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.woff2 b/vendor/assets/fonts/KaTeX_Main-Italic.woff2
new file mode 100644
index 00000000000..944e9740bdf
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Italic.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.eot b/vendor/assets/fonts/KaTeX_Main-Regular.eot
new file mode 100644
index 00000000000..dc60c090c7a
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.ttf b/vendor/assets/fonts/KaTeX_Main-Regular.ttf
new file mode 100644
index 00000000000..da5797ffcce
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.woff b/vendor/assets/fonts/KaTeX_Main-Regular.woff
new file mode 100644
index 00000000000..37db672e821
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.woff2 b/vendor/assets/fonts/KaTeX_Main-Regular.woff2
new file mode 100644
index 00000000000..48820424893
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Main-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.eot b/vendor/assets/fonts/KaTeX_Math-BoldItalic.eot
new file mode 100644
index 00000000000..52c8b8c6b40
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-BoldItalic.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.ttf b/vendor/assets/fonts/KaTeX_Math-BoldItalic.ttf
new file mode 100644
index 00000000000..a8b527c7ef6
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-BoldItalic.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff
new file mode 100644
index 00000000000..8940e0b5801
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2 b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2
new file mode 100644
index 00000000000..15cf56d3408
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.eot b/vendor/assets/fonts/KaTeX_Math-Italic.eot
new file mode 100644
index 00000000000..64c8992c477
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-Italic.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.ttf b/vendor/assets/fonts/KaTeX_Math-Italic.ttf
new file mode 100644
index 00000000000..06f39d3a299
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-Italic.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.woff b/vendor/assets/fonts/KaTeX_Math-Italic.woff
new file mode 100644
index 00000000000..cf3b4b79e5b
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-Italic.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.woff2 b/vendor/assets/fonts/KaTeX_Math-Italic.woff2
new file mode 100644
index 00000000000..5f8c4bfa455
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-Italic.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.eot b/vendor/assets/fonts/KaTeX_Math-Regular.eot
new file mode 100644
index 00000000000..5521e6a564d
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.ttf b/vendor/assets/fonts/KaTeX_Math-Regular.ttf
new file mode 100644
index 00000000000..73127082370
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.woff b/vendor/assets/fonts/KaTeX_Math-Regular.woff
new file mode 100644
index 00000000000..0e2ebdf18af
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.woff2 b/vendor/assets/fonts/KaTeX_Math-Regular.woff2
new file mode 100644
index 00000000000..ebe3d028a34
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Math-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.eot b/vendor/assets/fonts/KaTeX_SansSerif-Bold.eot
new file mode 100644
index 00000000000..1660e76a2b6
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Bold.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.ttf b/vendor/assets/fonts/KaTeX_SansSerif-Bold.ttf
new file mode 100644
index 00000000000..dbeb7b92ab5
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Bold.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff
new file mode 100644
index 00000000000..8f144a8bb31
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2 b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2
new file mode 100644
index 00000000000..329e85557fa
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.eot b/vendor/assets/fonts/KaTeX_SansSerif-Italic.eot
new file mode 100644
index 00000000000..289ae3ff8b7
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Italic.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.ttf b/vendor/assets/fonts/KaTeX_SansSerif-Italic.ttf
new file mode 100644
index 00000000000..b3a2f38f224
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Italic.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff
new file mode 100644
index 00000000000..bddf7ea6579
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2 b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2
new file mode 100644
index 00000000000..5fa767bddd6
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.eot b/vendor/assets/fonts/KaTeX_SansSerif-Regular.eot
new file mode 100644
index 00000000000..1b38b98a180
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.ttf b/vendor/assets/fonts/KaTeX_SansSerif-Regular.ttf
new file mode 100644
index 00000000000..e4712f84775
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff
new file mode 100644
index 00000000000..33be368048f
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2 b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2
new file mode 100644
index 00000000000..4fcb2e29a05
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.eot b/vendor/assets/fonts/KaTeX_Script-Regular.eot
new file mode 100644
index 00000000000..7870d7f319b
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Script-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.ttf b/vendor/assets/fonts/KaTeX_Script-Regular.ttf
new file mode 100644
index 00000000000..da4d11308ae
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Script-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.woff b/vendor/assets/fonts/KaTeX_Script-Regular.woff
new file mode 100644
index 00000000000..d6ae79f998a
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Script-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.woff2 b/vendor/assets/fonts/KaTeX_Script-Regular.woff2
new file mode 100644
index 00000000000..1b43deb45a8
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Script-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.eot b/vendor/assets/fonts/KaTeX_Size1-Regular.eot
new file mode 100644
index 00000000000..29950f95ff6
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size1-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.ttf b/vendor/assets/fonts/KaTeX_Size1-Regular.ttf
new file mode 100644
index 00000000000..194466a655d
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size1-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.woff b/vendor/assets/fonts/KaTeX_Size1-Regular.woff
new file mode 100644
index 00000000000..237f271edd1
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size1-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size1-Regular.woff2
new file mode 100644
index 00000000000..39b6f8f746c
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size1-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.eot b/vendor/assets/fonts/KaTeX_Size2-Regular.eot
new file mode 100644
index 00000000000..b8b0536f967
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size2-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.ttf b/vendor/assets/fonts/KaTeX_Size2-Regular.ttf
new file mode 100644
index 00000000000..b41b66a638f
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size2-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.woff b/vendor/assets/fonts/KaTeX_Size2-Regular.woff
new file mode 100644
index 00000000000..4a3055854ed
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size2-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size2-Regular.woff2
new file mode 100644
index 00000000000..3facec1ab89
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size2-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.eot b/vendor/assets/fonts/KaTeX_Size3-Regular.eot
new file mode 100644
index 00000000000..576b864fae6
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size3-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.ttf b/vendor/assets/fonts/KaTeX_Size3-Regular.ttf
new file mode 100644
index 00000000000..790ddbbc55f
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size3-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.woff b/vendor/assets/fonts/KaTeX_Size3-Regular.woff
new file mode 100644
index 00000000000..3a6d062e660
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size3-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size3-Regular.woff2
new file mode 100644
index 00000000000..2cffafe5018
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size3-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.eot b/vendor/assets/fonts/KaTeX_Size4-Regular.eot
new file mode 100644
index 00000000000..c2b045fc3db
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size4-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.ttf b/vendor/assets/fonts/KaTeX_Size4-Regular.ttf
new file mode 100644
index 00000000000..ce660aa7ff9
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size4-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.woff b/vendor/assets/fonts/KaTeX_Size4-Regular.woff
new file mode 100644
index 00000000000..7826c6c97a1
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size4-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size4-Regular.woff2
new file mode 100644
index 00000000000..c92189812d9
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Size4-Regular.woff2
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.eot b/vendor/assets/fonts/KaTeX_Typewriter-Regular.eot
new file mode 100644
index 00000000000..4c178f484a8
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Typewriter-Regular.eot
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.ttf b/vendor/assets/fonts/KaTeX_Typewriter-Regular.ttf
new file mode 100644
index 00000000000..b0427ad0a56
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Typewriter-Regular.ttf
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff
new file mode 100644
index 00000000000..78e990488a9
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff
Binary files differ
diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2 b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2
new file mode 100644
index 00000000000..618de99d480
--- /dev/null
+++ b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2
Binary files differ
diff --git a/vendor/assets/javascripts/es6-promise.auto.js b/vendor/assets/javascripts/es6-promise.auto.js
new file mode 100644
index 00000000000..19e6c13a655
--- /dev/null
+++ b/vendor/assets/javascripts/es6-promise.auto.js
@@ -0,0 +1,1159 @@
+/*!
+ * @overview es6-promise - a tiny implementation of Promises/A+.
+ * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald)
+ * @license Licensed under MIT license
+ * See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE
+ * @version 4.0.5
+ */
+
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.ES6Promise = factory());
+}(this, (function () { 'use strict';
+
+function objectOrFunction(x) {
+ return typeof x === 'function' || typeof x === 'object' && x !== null;
+}
+
+function isFunction(x) {
+ return typeof x === 'function';
+}
+
+var _isArray = undefined;
+if (!Array.isArray) {
+ _isArray = function (x) {
+ return Object.prototype.toString.call(x) === '[object Array]';
+ };
+} else {
+ _isArray = Array.isArray;
+}
+
+var isArray = _isArray;
+
+var len = 0;
+var vertxNext = undefined;
+var customSchedulerFn = undefined;
+
+var asap = function asap(callback, arg) {
+ queue[len] = callback;
+ queue[len + 1] = arg;
+ len += 2;
+ if (len === 2) {
+ // If len is 2, that means that we need to schedule an async flush.
+ // If additional callbacks are queued before the queue is flushed, they
+ // will be processed by this flush that we are scheduling.
+ if (customSchedulerFn) {
+ customSchedulerFn(flush);
+ } else {
+ scheduleFlush();
+ }
+ }
+};
+
+function setScheduler(scheduleFn) {
+ customSchedulerFn = scheduleFn;
+}
+
+function setAsap(asapFn) {
+ asap = asapFn;
+}
+
+var browserWindow = typeof window !== 'undefined' ? window : undefined;
+var browserGlobal = browserWindow || {};
+var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver;
+var isNode = typeof self === 'undefined' && typeof process !== 'undefined' && ({}).toString.call(process) === '[object process]';
+
+// test for web worker but not in IE10
+var isWorker = typeof Uint8ClampedArray !== 'undefined' && typeof importScripts !== 'undefined' && typeof MessageChannel !== 'undefined';
+
+// node
+function useNextTick() {
+ // node version 0.10.x displays a deprecation warning when nextTick is used recursively
+ // see https://github.com/cujojs/when/issues/410 for details
+ return function () {
+ return process.nextTick(flush);
+ };
+}
+
+// vertx
+function useVertxTimer() {
+ if (typeof vertxNext !== 'undefined') {
+ return function () {
+ vertxNext(flush);
+ };
+ }
+
+ return useSetTimeout();
+}
+
+function useMutationObserver() {
+ var iterations = 0;
+ var observer = new BrowserMutationObserver(flush);
+ var node = document.createTextNode('');
+ observer.observe(node, { characterData: true });
+
+ return function () {
+ node.data = iterations = ++iterations % 2;
+ };
+}
+
+// web worker
+function useMessageChannel() {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = flush;
+ return function () {
+ return channel.port2.postMessage(0);
+ };
+}
+
+function useSetTimeout() {
+ // Store setTimeout reference so es6-promise will be unaffected by
+ // other code modifying setTimeout (like sinon.useFakeTimers())
+ var globalSetTimeout = setTimeout;
+ return function () {
+ return globalSetTimeout(flush, 1);
+ };
+}
+
+var queue = new Array(1000);
+function flush() {
+ for (var i = 0; i < len; i += 2) {
+ var callback = queue[i];
+ var arg = queue[i + 1];
+
+ callback(arg);
+
+ queue[i] = undefined;
+ queue[i + 1] = undefined;
+ }
+
+ len = 0;
+}
+
+function attemptVertx() {
+ try {
+ var r = require;
+ var vertx = r('vertx');
+ vertxNext = vertx.runOnLoop || vertx.runOnContext;
+ return useVertxTimer();
+ } catch (e) {
+ return useSetTimeout();
+ }
+}
+
+var scheduleFlush = undefined;
+// Decide what async method to use to triggering processing of queued callbacks:
+if (isNode) {
+ scheduleFlush = useNextTick();
+} else if (BrowserMutationObserver) {
+ scheduleFlush = useMutationObserver();
+} else if (isWorker) {
+ scheduleFlush = useMessageChannel();
+} else if (browserWindow === undefined && typeof require === 'function') {
+ scheduleFlush = attemptVertx();
+} else {
+ scheduleFlush = useSetTimeout();
+}
+
+function then(onFulfillment, onRejection) {
+ var _arguments = arguments;
+
+ var parent = this;
+
+ var child = new this.constructor(noop);
+
+ if (child[PROMISE_ID] === undefined) {
+ makePromise(child);
+ }
+
+ var _state = parent._state;
+
+ if (_state) {
+ (function () {
+ var callback = _arguments[_state - 1];
+ asap(function () {
+ return invokeCallback(_state, child, callback, parent._result);
+ });
+ })();
+ } else {
+ subscribe(parent, child, onFulfillment, onRejection);
+ }
+
+ return child;
+}
+
+/**
+ `Promise.resolve` returns a promise that will become resolved with the
+ passed `value`. It is shorthand for the following:
+
+ ```javascript
+ let promise = new Promise(function(resolve, reject){
+ resolve(1);
+ });
+
+ promise.then(function(value){
+ // value === 1
+ });
+ ```
+
+ Instead of writing the above, your code now simply becomes the following:
+
+ ```javascript
+ let promise = Promise.resolve(1);
+
+ promise.then(function(value){
+ // value === 1
+ });
+ ```
+
+ @method resolve
+ @static
+ @param {Any} value value that the returned promise will be resolved with
+ Useful for tooling.
+ @return {Promise} a promise that will become fulfilled with the given
+ `value`
+*/
+function resolve(object) {
+ /*jshint validthis:true */
+ var Constructor = this;
+
+ if (object && typeof object === 'object' && object.constructor === Constructor) {
+ return object;
+ }
+
+ var promise = new Constructor(noop);
+ _resolve(promise, object);
+ return promise;
+}
+
+var PROMISE_ID = Math.random().toString(36).substring(16);
+
+function noop() {}
+
+var PENDING = void 0;
+var FULFILLED = 1;
+var REJECTED = 2;
+
+var GET_THEN_ERROR = new ErrorObject();
+
+function selfFulfillment() {
+ return new TypeError("You cannot resolve a promise with itself");
+}
+
+function cannotReturnOwn() {
+ return new TypeError('A promises callback cannot return that same promise.');
+}
+
+function getThen(promise) {
+ try {
+ return promise.then;
+ } catch (error) {
+ GET_THEN_ERROR.error = error;
+ return GET_THEN_ERROR;
+ }
+}
+
+function tryThen(then, value, fulfillmentHandler, rejectionHandler) {
+ try {
+ then.call(value, fulfillmentHandler, rejectionHandler);
+ } catch (e) {
+ return e;
+ }
+}
+
+function handleForeignThenable(promise, thenable, then) {
+ asap(function (promise) {
+ var sealed = false;
+ var error = tryThen(then, thenable, function (value) {
+ if (sealed) {
+ return;
+ }
+ sealed = true;
+ if (thenable !== value) {
+ _resolve(promise, value);
+ } else {
+ fulfill(promise, value);
+ }
+ }, function (reason) {
+ if (sealed) {
+ return;
+ }
+ sealed = true;
+
+ _reject(promise, reason);
+ }, 'Settle: ' + (promise._label || ' unknown promise'));
+
+ if (!sealed && error) {
+ sealed = true;
+ _reject(promise, error);
+ }
+ }, promise);
+}
+
+function handleOwnThenable(promise, thenable) {
+ if (thenable._state === FULFILLED) {
+ fulfill(promise, thenable._result);
+ } else if (thenable._state === REJECTED) {
+ _reject(promise, thenable._result);
+ } else {
+ subscribe(thenable, undefined, function (value) {
+ return _resolve(promise, value);
+ }, function (reason) {
+ return _reject(promise, reason);
+ });
+ }
+}
+
+function handleMaybeThenable(promise, maybeThenable, then$$) {
+ if (maybeThenable.constructor === promise.constructor && then$$ === then && maybeThenable.constructor.resolve === resolve) {
+ handleOwnThenable(promise, maybeThenable);
+ } else {
+ if (then$$ === GET_THEN_ERROR) {
+ _reject(promise, GET_THEN_ERROR.error);
+ } else if (then$$ === undefined) {
+ fulfill(promise, maybeThenable);
+ } else if (isFunction(then$$)) {
+ handleForeignThenable(promise, maybeThenable, then$$);
+ } else {
+ fulfill(promise, maybeThenable);
+ }
+ }
+}
+
+function _resolve(promise, value) {
+ if (promise === value) {
+ _reject(promise, selfFulfillment());
+ } else if (objectOrFunction(value)) {
+ handleMaybeThenable(promise, value, getThen(value));
+ } else {
+ fulfill(promise, value);
+ }
+}
+
+function publishRejection(promise) {
+ if (promise._onerror) {
+ promise._onerror(promise._result);
+ }
+
+ publish(promise);
+}
+
+function fulfill(promise, value) {
+ if (promise._state !== PENDING) {
+ return;
+ }
+
+ promise._result = value;
+ promise._state = FULFILLED;
+
+ if (promise._subscribers.length !== 0) {
+ asap(publish, promise);
+ }
+}
+
+function _reject(promise, reason) {
+ if (promise._state !== PENDING) {
+ return;
+ }
+ promise._state = REJECTED;
+ promise._result = reason;
+
+ asap(publishRejection, promise);
+}
+
+function subscribe(parent, child, onFulfillment, onRejection) {
+ var _subscribers = parent._subscribers;
+ var length = _subscribers.length;
+
+ parent._onerror = null;
+
+ _subscribers[length] = child;
+ _subscribers[length + FULFILLED] = onFulfillment;
+ _subscribers[length + REJECTED] = onRejection;
+
+ if (length === 0 && parent._state) {
+ asap(publish, parent);
+ }
+}
+
+function publish(promise) {
+ var subscribers = promise._subscribers;
+ var settled = promise._state;
+
+ if (subscribers.length === 0) {
+ return;
+ }
+
+ var child = undefined,
+ callback = undefined,
+ detail = promise._result;
+
+ for (var i = 0; i < subscribers.length; i += 3) {
+ child = subscribers[i];
+ callback = subscribers[i + settled];
+
+ if (child) {
+ invokeCallback(settled, child, callback, detail);
+ } else {
+ callback(detail);
+ }
+ }
+
+ promise._subscribers.length = 0;
+}
+
+function ErrorObject() {
+ this.error = null;
+}
+
+var TRY_CATCH_ERROR = new ErrorObject();
+
+function tryCatch(callback, detail) {
+ try {
+ return callback(detail);
+ } catch (e) {
+ TRY_CATCH_ERROR.error = e;
+ return TRY_CATCH_ERROR;
+ }
+}
+
+function invokeCallback(settled, promise, callback, detail) {
+ var hasCallback = isFunction(callback),
+ value = undefined,
+ error = undefined,
+ succeeded = undefined,
+ failed = undefined;
+
+ if (hasCallback) {
+ value = tryCatch(callback, detail);
+
+ if (value === TRY_CATCH_ERROR) {
+ failed = true;
+ error = value.error;
+ value = null;
+ } else {
+ succeeded = true;
+ }
+
+ if (promise === value) {
+ _reject(promise, cannotReturnOwn());
+ return;
+ }
+ } else {
+ value = detail;
+ succeeded = true;
+ }
+
+ if (promise._state !== PENDING) {
+ // noop
+ } else if (hasCallback && succeeded) {
+ _resolve(promise, value);
+ } else if (failed) {
+ _reject(promise, error);
+ } else if (settled === FULFILLED) {
+ fulfill(promise, value);
+ } else if (settled === REJECTED) {
+ _reject(promise, value);
+ }
+}
+
+function initializePromise(promise, resolver) {
+ try {
+ resolver(function resolvePromise(value) {
+ _resolve(promise, value);
+ }, function rejectPromise(reason) {
+ _reject(promise, reason);
+ });
+ } catch (e) {
+ _reject(promise, e);
+ }
+}
+
+var id = 0;
+function nextId() {
+ return id++;
+}
+
+function makePromise(promise) {
+ promise[PROMISE_ID] = id++;
+ promise._state = undefined;
+ promise._result = undefined;
+ promise._subscribers = [];
+}
+
+function Enumerator(Constructor, input) {
+ this._instanceConstructor = Constructor;
+ this.promise = new Constructor(noop);
+
+ if (!this.promise[PROMISE_ID]) {
+ makePromise(this.promise);
+ }
+
+ if (isArray(input)) {
+ this._input = input;
+ this.length = input.length;
+ this._remaining = input.length;
+
+ this._result = new Array(this.length);
+
+ if (this.length === 0) {
+ fulfill(this.promise, this._result);
+ } else {
+ this.length = this.length || 0;
+ this._enumerate();
+ if (this._remaining === 0) {
+ fulfill(this.promise, this._result);
+ }
+ }
+ } else {
+ _reject(this.promise, validationError());
+ }
+}
+
+function validationError() {
+ return new Error('Array Methods must be provided an Array');
+};
+
+Enumerator.prototype._enumerate = function () {
+ var length = this.length;
+ var _input = this._input;
+
+ for (var i = 0; this._state === PENDING && i < length; i++) {
+ this._eachEntry(_input[i], i);
+ }
+};
+
+Enumerator.prototype._eachEntry = function (entry, i) {
+ var c = this._instanceConstructor;
+ var resolve$$ = c.resolve;
+
+ if (resolve$$ === resolve) {
+ var _then = getThen(entry);
+
+ if (_then === then && entry._state !== PENDING) {
+ this._settledAt(entry._state, i, entry._result);
+ } else if (typeof _then !== 'function') {
+ this._remaining--;
+ this._result[i] = entry;
+ } else if (c === Promise) {
+ var promise = new c(noop);
+ handleMaybeThenable(promise, entry, _then);
+ this._willSettleAt(promise, i);
+ } else {
+ this._willSettleAt(new c(function (resolve$$) {
+ return resolve$$(entry);
+ }), i);
+ }
+ } else {
+ this._willSettleAt(resolve$$(entry), i);
+ }
+};
+
+Enumerator.prototype._settledAt = function (state, i, value) {
+ var promise = this.promise;
+
+ if (promise._state === PENDING) {
+ this._remaining--;
+
+ if (state === REJECTED) {
+ _reject(promise, value);
+ } else {
+ this._result[i] = value;
+ }
+ }
+
+ if (this._remaining === 0) {
+ fulfill(promise, this._result);
+ }
+};
+
+Enumerator.prototype._willSettleAt = function (promise, i) {
+ var enumerator = this;
+
+ subscribe(promise, undefined, function (value) {
+ return enumerator._settledAt(FULFILLED, i, value);
+ }, function (reason) {
+ return enumerator._settledAt(REJECTED, i, reason);
+ });
+};
+
+/**
+ `Promise.all` accepts an array of promises, and returns a new promise which
+ is fulfilled with an array of fulfillment values for the passed promises, or
+ rejected with the reason of the first passed promise to be rejected. It casts all
+ elements of the passed iterable to promises as it runs this algorithm.
+
+ Example:
+
+ ```javascript
+ let promise1 = resolve(1);
+ let promise2 = resolve(2);
+ let promise3 = resolve(3);
+ let promises = [ promise1, promise2, promise3 ];
+
+ Promise.all(promises).then(function(array){
+ // The array here would be [ 1, 2, 3 ];
+ });
+ ```
+
+ If any of the `promises` given to `all` are rejected, the first promise
+ that is rejected will be given as an argument to the returned promises's
+ rejection handler. For example:
+
+ Example:
+
+ ```javascript
+ let promise1 = resolve(1);
+ let promise2 = reject(new Error("2"));
+ let promise3 = reject(new Error("3"));
+ let promises = [ promise1, promise2, promise3 ];
+
+ Promise.all(promises).then(function(array){
+ // Code here never runs because there are rejected promises!
+ }, function(error) {
+ // error.message === "2"
+ });
+ ```
+
+ @method all
+ @static
+ @param {Array} entries array of promises
+ @param {String} label optional string for labeling the promise.
+ Useful for tooling.
+ @return {Promise} promise that is fulfilled when all `promises` have been
+ fulfilled, or rejected if any of them become rejected.
+ @static
+*/
+function all(entries) {
+ return new Enumerator(this, entries).promise;
+}
+
+/**
+ `Promise.race` returns a new promise which is settled in the same way as the
+ first passed promise to settle.
+
+ Example:
+
+ ```javascript
+ let promise1 = new Promise(function(resolve, reject){
+ setTimeout(function(){
+ resolve('promise 1');
+ }, 200);
+ });
+
+ let promise2 = new Promise(function(resolve, reject){
+ setTimeout(function(){
+ resolve('promise 2');
+ }, 100);
+ });
+
+ Promise.race([promise1, promise2]).then(function(result){
+ // result === 'promise 2' because it was resolved before promise1
+ // was resolved.
+ });
+ ```
+
+ `Promise.race` is deterministic in that only the state of the first
+ settled promise matters. For example, even if other promises given to the
+ `promises` array argument are resolved, but the first settled promise has
+ become rejected before the other promises became fulfilled, the returned
+ promise will become rejected:
+
+ ```javascript
+ let promise1 = new Promise(function(resolve, reject){
+ setTimeout(function(){
+ resolve('promise 1');
+ }, 200);
+ });
+
+ let promise2 = new Promise(function(resolve, reject){
+ setTimeout(function(){
+ reject(new Error('promise 2'));
+ }, 100);
+ });
+
+ Promise.race([promise1, promise2]).then(function(result){
+ // Code here never runs
+ }, function(reason){
+ // reason.message === 'promise 2' because promise 2 became rejected before
+ // promise 1 became fulfilled
+ });
+ ```
+
+ An example real-world use case is implementing timeouts:
+
+ ```javascript
+ Promise.race([ajax('foo.json'), timeout(5000)])
+ ```
+
+ @method race
+ @static
+ @param {Array} promises array of promises to observe
+ Useful for tooling.
+ @return {Promise} a promise which settles in the same way as the first passed
+ promise to settle.
+*/
+function race(entries) {
+ /*jshint validthis:true */
+ var Constructor = this;
+
+ if (!isArray(entries)) {
+ return new Constructor(function (_, reject) {
+ return reject(new TypeError('You must pass an array to race.'));
+ });
+ } else {
+ return new Constructor(function (resolve, reject) {
+ var length = entries.length;
+ for (var i = 0; i < length; i++) {
+ Constructor.resolve(entries[i]).then(resolve, reject);
+ }
+ });
+ }
+}
+
+/**
+ `Promise.reject` returns a promise rejected with the passed `reason`.
+ It is shorthand for the following:
+
+ ```javascript
+ let promise = new Promise(function(resolve, reject){
+ reject(new Error('WHOOPS'));
+ });
+
+ promise.then(function(value){
+ // Code here doesn't run because the promise is rejected!
+ }, function(reason){
+ // reason.message === 'WHOOPS'
+ });
+ ```
+
+ Instead of writing the above, your code now simply becomes the following:
+
+ ```javascript
+ let promise = Promise.reject(new Error('WHOOPS'));
+
+ promise.then(function(value){
+ // Code here doesn't run because the promise is rejected!
+ }, function(reason){
+ // reason.message === 'WHOOPS'
+ });
+ ```
+
+ @method reject
+ @static
+ @param {Any} reason value that the returned promise will be rejected with.
+ Useful for tooling.
+ @return {Promise} a promise rejected with the given `reason`.
+*/
+function reject(reason) {
+ /*jshint validthis:true */
+ var Constructor = this;
+ var promise = new Constructor(noop);
+ _reject(promise, reason);
+ return promise;
+}
+
+function needsResolver() {
+ throw new TypeError('You must pass a resolver function as the first argument to the promise constructor');
+}
+
+function needsNew() {
+ throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");
+}
+
+/**
+ Promise objects represent the eventual result of an asynchronous operation. The
+ primary way of interacting with a promise is through its `then` method, which
+ registers callbacks to receive either a promise's eventual value or the reason
+ why the promise cannot be fulfilled.
+
+ Terminology
+ -----------
+
+ - `promise` is an object or function with a `then` method whose behavior conforms to this specification.
+ - `thenable` is an object or function that defines a `then` method.
+ - `value` is any legal JavaScript value (including undefined, a thenable, or a promise).
+ - `exception` is a value that is thrown using the throw statement.
+ - `reason` is a value that indicates why a promise was rejected.
+ - `settled` the final resting state of a promise, fulfilled or rejected.
+
+ A promise can be in one of three states: pending, fulfilled, or rejected.
+
+ Promises that are fulfilled have a fulfillment value and are in the fulfilled
+ state. Promises that are rejected have a rejection reason and are in the
+ rejected state. A fulfillment value is never a thenable.
+
+ Promises can also be said to *resolve* a value. If this value is also a
+ promise, then the original promise's settled state will match the value's
+ settled state. So a promise that *resolves* a promise that rejects will
+ itself reject, and a promise that *resolves* a promise that fulfills will
+ itself fulfill.
+
+
+ Basic Usage:
+ ------------
+
+ ```js
+ let promise = new Promise(function(resolve, reject) {
+ // on success
+ resolve(value);
+
+ // on failure
+ reject(reason);
+ });
+
+ promise.then(function(value) {
+ // on fulfillment
+ }, function(reason) {
+ // on rejection
+ });
+ ```
+
+ Advanced Usage:
+ ---------------
+
+ Promises shine when abstracting away asynchronous interactions such as
+ `XMLHttpRequest`s.
+
+ ```js
+ function getJSON(url) {
+ return new Promise(function(resolve, reject){
+ let xhr = new XMLHttpRequest();
+
+ xhr.open('GET', url);
+ xhr.onreadystatechange = handler;
+ xhr.responseType = 'json';
+ xhr.setRequestHeader('Accept', 'application/json');
+ xhr.send();
+
+ function handler() {
+ if (this.readyState === this.DONE) {
+ if (this.status === 200) {
+ resolve(this.response);
+ } else {
+ reject(new Error('getJSON: `' + url + '` failed with status: [' + this.status + ']'));
+ }
+ }
+ };
+ });
+ }
+
+ getJSON('/posts.json').then(function(json) {
+ // on fulfillment
+ }, function(reason) {
+ // on rejection
+ });
+ ```
+
+ Unlike callbacks, promises are great composable primitives.
+
+ ```js
+ Promise.all([
+ getJSON('/posts'),
+ getJSON('/comments')
+ ]).then(function(values){
+ values[0] // => postsJSON
+ values[1] // => commentsJSON
+
+ return values;
+ });
+ ```
+
+ @class Promise
+ @param {function} resolver
+ Useful for tooling.
+ @constructor
+*/
+function Promise(resolver) {
+ this[PROMISE_ID] = nextId();
+ this._result = this._state = undefined;
+ this._subscribers = [];
+
+ if (noop !== resolver) {
+ typeof resolver !== 'function' && needsResolver();
+ this instanceof Promise ? initializePromise(this, resolver) : needsNew();
+ }
+}
+
+Promise.all = all;
+Promise.race = race;
+Promise.resolve = resolve;
+Promise.reject = reject;
+Promise._setScheduler = setScheduler;
+Promise._setAsap = setAsap;
+Promise._asap = asap;
+
+Promise.prototype = {
+ constructor: Promise,
+
+ /**
+ The primary way of interacting with a promise is through its `then` method,
+ which registers callbacks to receive either a promise's eventual value or the
+ reason why the promise cannot be fulfilled.
+
+ ```js
+ findUser().then(function(user){
+ // user is available
+ }, function(reason){
+ // user is unavailable, and you are given the reason why
+ });
+ ```
+
+ Chaining
+ --------
+
+ The return value of `then` is itself a promise. This second, 'downstream'
+ promise is resolved with the return value of the first promise's fulfillment
+ or rejection handler, or rejected if the handler throws an exception.
+
+ ```js
+ findUser().then(function (user) {
+ return user.name;
+ }, function (reason) {
+ return 'default name';
+ }).then(function (userName) {
+ // If `findUser` fulfilled, `userName` will be the user's name, otherwise it
+ // will be `'default name'`
+ });
+
+ findUser().then(function (user) {
+ throw new Error('Found user, but still unhappy');
+ }, function (reason) {
+ throw new Error('`findUser` rejected and we're unhappy');
+ }).then(function (value) {
+ // never reached
+ }, function (reason) {
+ // if `findUser` fulfilled, `reason` will be 'Found user, but still unhappy'.
+ // If `findUser` rejected, `reason` will be '`findUser` rejected and we're unhappy'.
+ });
+ ```
+ If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream.
+
+ ```js
+ findUser().then(function (user) {
+ throw new PedagogicalException('Upstream error');
+ }).then(function (value) {
+ // never reached
+ }).then(function (value) {
+ // never reached
+ }, function (reason) {
+ // The `PedgagocialException` is propagated all the way down to here
+ });
+ ```
+
+ Assimilation
+ ------------
+
+ Sometimes the value you want to propagate to a downstream promise can only be
+ retrieved asynchronously. This can be achieved by returning a promise in the
+ fulfillment or rejection handler. The downstream promise will then be pending
+ until the returned promise is settled. This is called *assimilation*.
+
+ ```js
+ findUser().then(function (user) {
+ return findCommentsByAuthor(user);
+ }).then(function (comments) {
+ // The user's comments are now available
+ });
+ ```
+
+ If the assimliated promise rejects, then the downstream promise will also reject.
+
+ ```js
+ findUser().then(function (user) {
+ return findCommentsByAuthor(user);
+ }).then(function (comments) {
+ // If `findCommentsByAuthor` fulfills, we'll have the value here
+ }, function (reason) {
+ // If `findCommentsByAuthor` rejects, we'll have the reason here
+ });
+ ```
+
+ Simple Example
+ --------------
+
+ Synchronous Example
+
+ ```javascript
+ let result;
+
+ try {
+ result = findResult();
+ // success
+ } catch(reason) {
+ // failure
+ }
+ ```
+
+ Errback Example
+
+ ```js
+ findResult(function(result, err){
+ if (err) {
+ // failure
+ } else {
+ // success
+ }
+ });
+ ```
+
+ Promise Example;
+
+ ```javascript
+ findResult().then(function(result){
+ // success
+ }, function(reason){
+ // failure
+ });
+ ```
+
+ Advanced Example
+ --------------
+
+ Synchronous Example
+
+ ```javascript
+ let author, books;
+
+ try {
+ author = findAuthor();
+ books = findBooksByAuthor(author);
+ // success
+ } catch(reason) {
+ // failure
+ }
+ ```
+
+ Errback Example
+
+ ```js
+
+ function foundBooks(books) {
+
+ }
+
+ function failure(reason) {
+
+ }
+
+ findAuthor(function(author, err){
+ if (err) {
+ failure(err);
+ // failure
+ } else {
+ try {
+ findBoooksByAuthor(author, function(books, err) {
+ if (err) {
+ failure(err);
+ } else {
+ try {
+ foundBooks(books);
+ } catch(reason) {
+ failure(reason);
+ }
+ }
+ });
+ } catch(error) {
+ failure(err);
+ }
+ // success
+ }
+ });
+ ```
+
+ Promise Example;
+
+ ```javascript
+ findAuthor().
+ then(findBooksByAuthor).
+ then(function(books){
+ // found books
+ }).catch(function(reason){
+ // something went wrong
+ });
+ ```
+
+ @method then
+ @param {Function} onFulfilled
+ @param {Function} onRejected
+ Useful for tooling.
+ @return {Promise}
+ */
+ then: then,
+
+ /**
+ `catch` is simply sugar for `then(undefined, onRejection)` which makes it the same
+ as the catch block of a try/catch statement.
+
+ ```js
+ function findAuthor(){
+ throw new Error('couldn't find that author');
+ }
+
+ // synchronous
+ try {
+ findAuthor();
+ } catch(reason) {
+ // something went wrong
+ }
+
+ // async with promises
+ findAuthor().catch(function(reason){
+ // something went wrong
+ });
+ ```
+
+ @method catch
+ @param {Function} onRejection
+ Useful for tooling.
+ @return {Promise}
+ */
+ 'catch': function _catch(onRejection) {
+ return this.then(null, onRejection);
+ }
+};
+
+function polyfill() {
+ var local = undefined;
+
+ if (typeof global !== 'undefined') {
+ local = global;
+ } else if (typeof self !== 'undefined') {
+ local = self;
+ } else {
+ try {
+ local = Function('return this')();
+ } catch (e) {
+ throw new Error('polyfill failed because global object is unavailable in this environment');
+ }
+ }
+
+ var P = local.Promise;
+
+ if (P) {
+ var promiseToString = null;
+ try {
+ promiseToString = Object.prototype.toString.call(P.resolve());
+ } catch (e) {
+ // silently ignored
+ }
+
+ if (promiseToString === '[object Promise]' && !P.cast) {
+ return;
+ }
+ }
+
+ local.Promise = Promise;
+}
+
+// Strange compat..
+Promise.polyfill = polyfill;
+Promise.Promise = Promise;
+
+return Promise;
+
+})));
+
+ES6Promise.polyfill();
+//# sourceMappingURL=es6-promise.auto.map
diff --git a/vendor/assets/javascripts/jquery.timeago.js b/vendor/assets/javascripts/jquery.timeago.js
deleted file mode 100644
index de76cdd2ea7..00000000000
--- a/vendor/assets/javascripts/jquery.timeago.js
+++ /dev/null
@@ -1,182 +0,0 @@
-/* eslint-disable */
-/**
- * Timeago is a jQuery plugin that makes it easy to support automatically
- * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
- *
- * @name timeago
- * @version 1.1.0
- * @requires jQuery v1.2.3+
- * @author Ryan McGeary
- * @license MIT License - http://www.opensource.org/licenses/mit-license.php
- *
- * For usage and examples, visit:
- * http://timeago.yarp.com/
- *
- * Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
- */
-
-(function (factory) {
- if (typeof define === 'function' && define.amd) {
- // AMD. Register as an anonymous module.
- define(['jquery'], factory);
- } else {
- // Browser globals
- factory(jQuery);
- }
-}(function ($) {
- $.timeago = function(timestamp) {
- if (timestamp instanceof Date) {
- return inWords(timestamp);
- } else if (typeof timestamp === "string") {
- return inWords($.timeago.parse(timestamp));
- } else if (typeof timestamp === "number") {
- return inWords(new Date(timestamp));
- } else {
- return inWords($.timeago.datetime(timestamp));
- }
- };
- var $t = $.timeago;
-
- $.extend($.timeago, {
- settings: {
- refreshMillis: 60000,
- allowFuture: false,
- strings: {
- prefixAgo: null,
- prefixFromNow: null,
- suffixAgo: "ago",
- suffixFromNow: "from now",
- seconds: "less than a minute",
- minute: "about a minute",
- minutes: "%d minutes",
- hour: "about an hour",
- hours: "about %d hours",
- day: "a day",
- days: "%d days",
- month: "about a month",
- months: "%d months",
- year: "about a year",
- years: "%d years",
- wordSeparator: " ",
- numbers: []
- }
- },
- inWords: function(distanceMillis) {
- var $l = this.settings.strings;
- var prefix = $l.prefixAgo;
- var suffix = $l.suffixAgo;
- if (this.settings.allowFuture) {
- if (distanceMillis < 0) {
- prefix = $l.prefixFromNow;
- suffix = $l.suffixFromNow;
- }
- }
-
- var seconds = Math.abs(distanceMillis) / 1000;
- var minutes = seconds / 60;
- var hours = minutes / 60;
- var days = hours / 24;
- var years = days / 365;
-
- function substitute(stringOrFunction, number) {
- var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
- var value = ($l.numbers && $l.numbers[number]) || number;
- return string.replace(/%d/i, value);
- }
-
- var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
- seconds < 90 && substitute($l.minute, 1) ||
- minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
- minutes < 90 && substitute($l.hour, 1) ||
- hours < 24 && substitute($l.hours, Math.round(hours)) ||
- hours < 42 && substitute($l.day, 1) ||
- days < 30 && substitute($l.days, Math.round(days)) ||
- days < 45 && substitute($l.month, 1) ||
- days < 365 && substitute($l.months, Math.round(days / 30)) ||
- years < 1.5 && substitute($l.year, 1) ||
- substitute($l.years, Math.round(years));
-
- var separator = $l.wordSeparator || "";
- if ($l.wordSeparator === undefined) { separator = " "; }
- return $.trim([prefix, words, suffix].join(separator));
- },
- parse: function(iso8601) {
- var s = $.trim(iso8601);
- s = s.replace(/\.\d+/,""); // remove milliseconds
- s = s.replace(/-/,"/").replace(/-/,"/");
- s = s.replace(/T/," ").replace(/Z/," UTC");
- s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
- return new Date(s);
- },
- datetime: function(elem) {
- var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
- return $t.parse(iso8601);
- },
- isTime: function(elem) {
- // jQuery's `is()` doesn't play well with HTML5 in IE
- return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
- }
- });
-
- // functions that can be called via $(el).timeago('action')
- // init is default when no action is given
- // functions are called with context of a single element
- var functions = {
- init: function(){
- var refresh_el = $.proxy(refresh, this);
- refresh_el();
- var $s = $t.settings;
- if ($s.refreshMillis > 0) {
- setInterval(refresh_el, $s.refreshMillis);
- }
- },
- update: function(time){
- $(this).data('timeago', { datetime: $t.parse(time) });
- refresh.apply(this);
- }
- };
-
- $.fn.timeago = function(action, options) {
- var fn = action ? functions[action] : functions.init;
- if(!fn){
- throw new Error("Unknown function name '"+ action +"' for timeago");
- }
- // each over objects here and call the requested function
- this.each(function(){
- fn.call(this, options);
- });
- return this;
- };
-
- function refresh() {
- var data = prepareData(this);
- if (!isNaN(data.datetime)) {
- $(this).text(inWords(data.datetime));
- }
- return this;
- }
-
- function prepareData(element) {
- element = $(element);
- if (!element.data("timeago")) {
- element.data("timeago", { datetime: $t.datetime(element) });
- var text = $.trim(element.text());
- if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
- element.attr("title", text);
- }
- }
- return element.data("timeago");
- }
-
- function inWords(date) {
- return $t.inWords(distance(date));
- }
-
- function distance(date) {
- return (new Date().getTime() - date.getTime());
- }
-
- // fix for IE6 suckage
- document.createElement("abbr");
- document.createElement("time");
-}));
diff --git a/vendor/assets/javascripts/katex.js b/vendor/assets/javascripts/katex.js
new file mode 100644
index 00000000000..beb31ca6a7e
--- /dev/null
+++ b/vendor/assets/javascripts/katex.js
@@ -0,0 +1,8683 @@
+/*
+ The MIT License (MIT)
+
+ Copyright (c) 2015 Khan Academy
+
+ This software also uses portions of the underscore.js project, which is
+ MIT licensed with the following copyright:
+
+ Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative
+ Reporters & Editors
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ */
+
+/*
+ Here is how to build a version of KaTeX that works with gitlab.
+
+ The problem is that the standard procedure for changing font location doesn't work for the empty string.
+
+ 1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do.
+ 2. make (requires node)
+ 3. sed -i 's,fonts/,,' build/katex.css
+ 4. Copy build/katex.js, build/katex.css and fonts/* to gitlab.
+*/
+
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.katex = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/* eslint no-console:0 */
+/**
+ * This is the main entry point for KaTeX. Here, we expose functions for
+ * rendering expressions either to DOM nodes or to markup strings.
+ *
+ * We also expose the ParseError class to check if errors thrown from KaTeX are
+ * errors in the expression, or errors in javascript handling.
+ */
+
+var ParseError = require("./src/ParseError");
+var Settings = require("./src/Settings");
+
+var buildTree = require("./src/buildTree");
+var parseTree = require("./src/parseTree");
+var utils = require("./src/utils");
+
+/**
+ * Parse and build an expression, and place that expression in the DOM node
+ * given.
+ */
+var render = function(expression, baseNode, options) {
+ utils.clearNode(baseNode);
+
+ var settings = new Settings(options);
+
+ var tree = parseTree(expression, settings);
+ var node = buildTree(tree, expression, settings).toNode();
+
+ baseNode.appendChild(node);
+};
+
+// KaTeX's styles don't work properly in quirks mode. Print out an error, and
+// disable rendering.
+if (typeof document !== "undefined") {
+ if (document.compatMode !== "CSS1Compat") {
+ typeof console !== "undefined" && console.warn(
+ "Warning: KaTeX doesn't work in quirks mode. Make sure your " +
+ "website has a suitable doctype.");
+
+ render = function() {
+ throw new ParseError("KaTeX doesn't work in quirks mode.");
+ };
+ }
+}
+
+/**
+ * Parse and build an expression, and return the markup for that.
+ */
+var renderToString = function(expression, options) {
+ var settings = new Settings(options);
+
+ var tree = parseTree(expression, settings);
+ return buildTree(tree, expression, settings).toMarkup();
+};
+
+/**
+ * Parse an expression and return the parse tree.
+ */
+var generateParseTree = function(expression, options) {
+ var settings = new Settings(options);
+ return parseTree(expression, settings);
+};
+
+module.exports = {
+ render: render,
+ renderToString: renderToString,
+ /**
+ * NOTE: This method is not currently recommended for public use.
+ * The internal tree representation is unstable and is very likely
+ * to change. Use at your own risk.
+ */
+ __parse: generateParseTree,
+ ParseError: ParseError,
+};
+
+},{"./src/ParseError":6,"./src/Settings":8,"./src/buildTree":13,"./src/parseTree":22,"./src/utils":25}],2:[function(require,module,exports){
+/** @flow */
+
+"use strict";
+
+function getRelocatable(re) {
+ // In the future, this could use a WeakMap instead of an expando.
+ if (!re.__matchAtRelocatable) {
+ // Disjunctions are the lowest-precedence operator, so we can make any
+ // pattern match the empty string by appending `|()` to it:
+ // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-patterns
+ var source = re.source + "|()";
+
+ // We always make the new regex global.
+ var flags = "g" + (re.ignoreCase ? "i" : "") + (re.multiline ? "m" : "") + (re.unicode ? "u" : "")
+ // sticky (/.../y) doesn't make sense in conjunction with our relocation
+ // logic, so we ignore it here.
+ ;
+
+ re.__matchAtRelocatable = new RegExp(source, flags);
+ }
+ return re.__matchAtRelocatable;
+}
+
+function matchAt(re, str, pos) {
+ if (re.global || re.sticky) {
+ throw new Error("matchAt(...): Only non-global regexes are supported");
+ }
+ var reloc = getRelocatable(re);
+ reloc.lastIndex = pos;
+ var match = reloc.exec(str);
+ // Last capturing group is our sentinel that indicates whether the regex
+ // matched at the given location.
+ if (match[match.length - 1] == null) {
+ // Original regex matched.
+ match.length = match.length - 1;
+ return match;
+ } else {
+ return null;
+ }
+}
+
+module.exports = matchAt;
+},{}],3:[function(require,module,exports){
+/**
+ * The Lexer class handles tokenizing the input in various ways. Since our
+ * parser expects us to be able to backtrack, the lexer allows lexing from any
+ * given starting point.
+ *
+ * Its main exposed function is the `lex` function, which takes a position to
+ * lex from and a type of token to lex. It defers to the appropriate `_innerLex`
+ * function.
+ *
+ * The various `_innerLex` functions perform the actual lexing of different
+ * kinds.
+ */
+
+var matchAt = require("match-at");
+
+var ParseError = require("./ParseError");
+
+// The main lexer class
+function Lexer(input) {
+ this.input = input;
+ this.pos = 0;
+}
+
+/**
+ * The resulting token returned from `lex`.
+ *
+ * It consists of the token text plus some position information.
+ * The position information is essentially a range in an input string,
+ * but instead of referencing the bare input string, we refer to the lexer.
+ * That way it is possible to attach extra metadata to the input string,
+ * like for example a file name or similar.
+ *
+ * The position information (all three parameters) is optional,
+ * so it is OK to construct synthetic tokens if appropriate.
+ * Not providing available position information may lead to
+ * degraded error reporting, though.
+ *
+ * @param {string} text the text of this token
+ * @param {number=} start the start offset, zero-based inclusive
+ * @param {number=} end the end offset, zero-based exclusive
+ * @param {Lexer=} lexer the lexer which in turn holds the input string
+ */
+function Token(text, start, end, lexer) {
+ this.text = text;
+ this.start = start;
+ this.end = end;
+ this.lexer = lexer;
+}
+
+/**
+ * Given a pair of tokens (this and endToken), compute a “Token” encompassing
+ * the whole input range enclosed by these two.
+ *
+ * @param {Token} endToken last token of the range, inclusive
+ * @param {string} text the text of the newly constructed token
+ */
+Token.prototype.range = function(endToken, text) {
+ if (endToken.lexer !== this.lexer) {
+ return new Token(text); // sorry, no position information available
+ }
+ return new Token(text, this.start, endToken.end, this.lexer);
+};
+
+/* The following tokenRegex
+ * - matches typical whitespace (but not NBSP etc.) using its first group
+ * - does not match any control character \x00-\x1f except whitespace
+ * - does not match a bare backslash
+ * - matches any ASCII character except those just mentioned
+ * - does not match the BMP private use area \uE000-\uF8FF
+ * - does not match bare surrogate code units
+ * - matches any BMP character except for those just described
+ * - matches any valid Unicode surrogate pair
+ * - matches a backslash followed by one or more letters
+ * - matches a backslash followed by any BMP character, including newline
+ * Just because the Lexer matches something doesn't mean it's valid input:
+ * If there is no matching function or symbol definition, the Parser will
+ * still reject the input.
+ */
+var tokenRegex = new RegExp(
+ "([ \r\n\t]+)|" + // whitespace
+ "([!-\\[\\]-\u2027\u202A-\uD7FF\uF900-\uFFFF]" + // single codepoint
+ "|[\uD800-\uDBFF][\uDC00-\uDFFF]" + // surrogate pair
+ "|\\\\(?:[a-zA-Z]+|[^\uD800-\uDFFF])" + // function name
+ ")"
+);
+
+/**
+ * This function lexes a single token.
+ */
+Lexer.prototype.lex = function() {
+ var input = this.input;
+ var pos = this.pos;
+ if (pos === input.length) {
+ return new Token("EOF", pos, pos, this);
+ }
+ var match = matchAt(tokenRegex, input, pos);
+ if (match === null) {
+ throw new ParseError(
+ "Unexpected character: '" + input[pos] + "'",
+ new Token(input[pos], pos, pos + 1, this));
+ }
+ var text = match[2] || " ";
+ var start = this.pos;
+ this.pos += match[0].length;
+ var end = this.pos;
+ return new Token(text, start, end, this);
+};
+
+module.exports = Lexer;
+
+},{"./ParseError":6,"match-at":2}],4:[function(require,module,exports){
+/**
+ * This file contains the “gullet” where macros are expanded
+ * until only non-macro tokens remain.
+ */
+
+var Lexer = require("./Lexer");
+
+function MacroExpander(input, macros) {
+ this.lexer = new Lexer(input);
+ this.macros = macros;
+ this.stack = []; // contains tokens in REVERSE order
+ this.discardedWhiteSpace = [];
+}
+
+/**
+ * Recursively expand first token, then return first non-expandable token.
+ */
+MacroExpander.prototype.nextToken = function() {
+ for (;;) {
+ if (this.stack.length === 0) {
+ this.stack.push(this.lexer.lex());
+ }
+ var topToken = this.stack.pop();
+ var name = topToken.text;
+ if (!(name.charAt(0) === "\\" && this.macros.hasOwnProperty(name))) {
+ return topToken;
+ }
+ var expansion = this.macros[name];
+ if (typeof expansion === "string") {
+ var bodyLexer = new Lexer(expansion);
+ expansion = [];
+ var tok = bodyLexer.lex();
+ while (tok.text !== "EOF") {
+ expansion.push(tok);
+ tok = bodyLexer.lex();
+ }
+ expansion.reverse(); // to fit in with stack using push and pop
+ this.macros[name] = expansion;
+ }
+ this.stack = this.stack.concat(expansion);
+ }
+};
+
+MacroExpander.prototype.get = function(ignoreSpace) {
+ this.discardedWhiteSpace = [];
+ var token = this.nextToken();
+ if (ignoreSpace) {
+ while (token.text === " ") {
+ this.discardedWhiteSpace.push(token);
+ token = this.nextToken();
+ }
+ }
+ return token;
+};
+
+/**
+ * Undo the effect of the preceding call to the get method.
+ * A call to this method MUST be immediately preceded and immediately followed
+ * by a call to get. Only used during mode switching, i.e. after one token
+ * was got in the old mode but should get got again in a new mode
+ * with possibly different whitespace handling.
+ */
+MacroExpander.prototype.unget = function(token) {
+ this.stack.push(token);
+ while (this.discardedWhiteSpace.length !== 0) {
+ this.stack.push(this.discardedWhiteSpace.pop());
+ }
+};
+
+module.exports = MacroExpander;
+
+},{"./Lexer":3}],5:[function(require,module,exports){
+/**
+ * This file contains information about the options that the Parser carries
+ * around with it while parsing. Data is held in an `Options` object, and when
+ * recursing, a new `Options` object can be created with the `.with*` and
+ * `.reset` functions.
+ */
+
+/**
+ * This is the main options class. It contains the style, size, color, and font
+ * of the current parse level. It also contains the style and size of the parent
+ * parse level, so size changes can be handled efficiently.
+ *
+ * Each of the `.with*` and `.reset` functions passes its current style and size
+ * as the parentStyle and parentSize of the new options class, so parent
+ * handling is taken care of automatically.
+ */
+function Options(data) {
+ this.style = data.style;
+ this.color = data.color;
+ this.size = data.size;
+ this.phantom = data.phantom;
+ this.font = data.font;
+
+ if (data.parentStyle === undefined) {
+ this.parentStyle = data.style;
+ } else {
+ this.parentStyle = data.parentStyle;
+ }
+
+ if (data.parentSize === undefined) {
+ this.parentSize = data.size;
+ } else {
+ this.parentSize = data.parentSize;
+ }
+}
+
+/**
+ * Returns a new options object with the same properties as "this". Properties
+ * from "extension" will be copied to the new options object.
+ */
+Options.prototype.extend = function(extension) {
+ var data = {
+ style: this.style,
+ size: this.size,
+ color: this.color,
+ parentStyle: this.style,
+ parentSize: this.size,
+ phantom: this.phantom,
+ font: this.font,
+ };
+
+ for (var key in extension) {
+ if (extension.hasOwnProperty(key)) {
+ data[key] = extension[key];
+ }
+ }
+
+ return new Options(data);
+};
+
+/**
+ * Create a new options object with the given style.
+ */
+Options.prototype.withStyle = function(style) {
+ return this.extend({
+ style: style,
+ });
+};
+
+/**
+ * Create a new options object with the given size.
+ */
+Options.prototype.withSize = function(size) {
+ return this.extend({
+ size: size,
+ });
+};
+
+/**
+ * Create a new options object with the given color.
+ */
+Options.prototype.withColor = function(color) {
+ return this.extend({
+ color: color,
+ });
+};
+
+/**
+ * Create a new options object with "phantom" set to true.
+ */
+Options.prototype.withPhantom = function() {
+ return this.extend({
+ phantom: true,
+ });
+};
+
+/**
+ * Create a new options objects with the give font.
+ */
+Options.prototype.withFont = function(font) {
+ return this.extend({
+ font: font,
+ });
+};
+
+/**
+ * Create a new options object with the same style, size, and color. This is
+ * used so that parent style and size changes are handled correctly.
+ */
+Options.prototype.reset = function() {
+ return this.extend({});
+};
+
+/**
+ * A map of color names to CSS colors.
+ * TODO(emily): Remove this when we have real macros
+ */
+var colorMap = {
+ "katex-blue": "#6495ed",
+ "katex-orange": "#ffa500",
+ "katex-pink": "#ff00af",
+ "katex-red": "#df0030",
+ "katex-green": "#28ae7b",
+ "katex-gray": "gray",
+ "katex-purple": "#9d38bd",
+ "katex-blueA": "#ccfaff",
+ "katex-blueB": "#80f6ff",
+ "katex-blueC": "#63d9ea",
+ "katex-blueD": "#11accd",
+ "katex-blueE": "#0c7f99",
+ "katex-tealA": "#94fff5",
+ "katex-tealB": "#26edd5",
+ "katex-tealC": "#01d1c1",
+ "katex-tealD": "#01a995",
+ "katex-tealE": "#208170",
+ "katex-greenA": "#b6ffb0",
+ "katex-greenB": "#8af281",
+ "katex-greenC": "#74cf70",
+ "katex-greenD": "#1fab54",
+ "katex-greenE": "#0d923f",
+ "katex-goldA": "#ffd0a9",
+ "katex-goldB": "#ffbb71",
+ "katex-goldC": "#ff9c39",
+ "katex-goldD": "#e07d10",
+ "katex-goldE": "#a75a05",
+ "katex-redA": "#fca9a9",
+ "katex-redB": "#ff8482",
+ "katex-redC": "#f9685d",
+ "katex-redD": "#e84d39",
+ "katex-redE": "#bc2612",
+ "katex-maroonA": "#ffbde0",
+ "katex-maroonB": "#ff92c6",
+ "katex-maroonC": "#ed5fa6",
+ "katex-maroonD": "#ca337c",
+ "katex-maroonE": "#9e034e",
+ "katex-purpleA": "#ddd7ff",
+ "katex-purpleB": "#c6b9fc",
+ "katex-purpleC": "#aa87ff",
+ "katex-purpleD": "#7854ab",
+ "katex-purpleE": "#543b78",
+ "katex-mintA": "#f5f9e8",
+ "katex-mintB": "#edf2df",
+ "katex-mintC": "#e0e5cc",
+ "katex-grayA": "#f6f7f7",
+ "katex-grayB": "#f0f1f2",
+ "katex-grayC": "#e3e5e6",
+ "katex-grayD": "#d6d8da",
+ "katex-grayE": "#babec2",
+ "katex-grayF": "#888d93",
+ "katex-grayG": "#626569",
+ "katex-grayH": "#3b3e40",
+ "katex-grayI": "#21242c",
+ "katex-kaBlue": "#314453",
+ "katex-kaGreen": "#71B307",
+};
+
+/**
+ * Gets the CSS color of the current options object, accounting for the
+ * `colorMap`.
+ */
+Options.prototype.getColor = function() {
+ if (this.phantom) {
+ return "transparent";
+ } else {
+ return colorMap[this.color] || this.color;
+ }
+};
+
+module.exports = Options;
+
+},{}],6:[function(require,module,exports){
+/**
+ * This is the ParseError class, which is the main error thrown by KaTeX
+ * functions when something has gone wrong. This is used to distinguish internal
+ * errors from errors in the expression that the user provided.
+ *
+ * If possible, a caller should provide a Token or ParseNode with information
+ * about where in the source string the problem occurred.
+ *
+ * @param {string} message The error message
+ * @param {(Token|ParseNode)=} token An object providing position information
+ */
+function ParseError(message, token) {
+ var error = "KaTeX parse error: " + message;
+ var start;
+ var end;
+
+ if (token && token.lexer && token.start <= token.end) {
+ // If we have the input and a position, make the error a bit fancier
+
+ // Get the input
+ var input = token.lexer.input;
+
+ // Prepend some information
+ start = token.start;
+ end = token.end;
+ if (start === input.length) {
+ error += " at end of input: ";
+ } else {
+ error += " at position " + (start + 1) + ": ";
+ }
+
+ // Underline token in question using combining underscores
+ var underlined = input.slice(start, end).replace(/[^]/g, "$&\u0332");
+
+ // Extract some context from the input and add it to the error
+ var left;
+ if (start > 15) {
+ left = "…" + input.slice(start - 15, start);
+ } else {
+ left = input.slice(0, start);
+ }
+ var right;
+ if (end + 15 < input.length) {
+ right = input.slice(end, end + 15) + "…";
+ } else {
+ right = input.slice(end);
+ }
+ error += left + underlined + right;
+ }
+
+ // Some hackery to make ParseError a prototype of Error
+ // See http://stackoverflow.com/a/8460753
+ var self = new Error(error);
+ self.name = "ParseError";
+ self.__proto__ = ParseError.prototype;
+
+ self.position = start;
+ return self;
+}
+
+// More hackery
+ParseError.prototype.__proto__ = Error.prototype;
+
+module.exports = ParseError;
+
+},{}],7:[function(require,module,exports){
+/* eslint no-constant-condition:0 */
+var functions = require("./functions");
+var environments = require("./environments");
+var MacroExpander = require("./MacroExpander");
+var symbols = require("./symbols");
+var utils = require("./utils");
+var cjkRegex = require("./unicodeRegexes").cjkRegex;
+
+var parseData = require("./parseData");
+var ParseError = require("./ParseError");
+
+/**
+ * This file contains the parser used to parse out a TeX expression from the
+ * input. Since TeX isn't context-free, standard parsers don't work particularly
+ * well.
+ *
+ * The strategy of this parser is as such:
+ *
+ * The main functions (the `.parse...` ones) take a position in the current
+ * parse string to parse tokens from. The lexer (found in Lexer.js, stored at
+ * this.lexer) also supports pulling out tokens at arbitrary places. When
+ * individual tokens are needed at a position, the lexer is called to pull out a
+ * token, which is then used.
+ *
+ * The parser has a property called "mode" indicating the mode that
+ * the parser is currently in. Currently it has to be one of "math" or
+ * "text", which denotes whether the current environment is a math-y
+ * one or a text-y one (e.g. inside \text). Currently, this serves to
+ * limit the functions which can be used in text mode.
+ *
+ * The main functions then return an object which contains the useful data that
+ * was parsed at its given point, and a new position at the end of the parsed
+ * data. The main functions can call each other and continue the parsing by
+ * using the returned position as a new starting point.
+ *
+ * There are also extra `.handle...` functions, which pull out some reused
+ * functionality into self-contained functions.
+ *
+ * The earlier functions return ParseNodes.
+ * The later functions (which are called deeper in the parse) sometimes return
+ * ParseFuncOrArgument, which contain a ParseNode as well as some data about
+ * whether the parsed object is a function which is missing some arguments, or a
+ * standalone object which can be used as an argument to another function.
+ */
+
+/**
+ * Main Parser class
+ */
+function Parser(input, settings) {
+ // Create a new macro expander (gullet) and (indirectly via that) also a
+ // new lexer (mouth) for this parser (stomach, in the language of TeX)
+ this.gullet = new MacroExpander(input, settings.macros);
+ // Store the settings for use in parsing
+ this.settings = settings;
+}
+
+var ParseNode = parseData.ParseNode;
+
+/**
+ * An initial function (without its arguments), or an argument to a function.
+ * The `result` argument should be a ParseNode.
+ */
+function ParseFuncOrArgument(result, isFunction, token) {
+ this.result = result;
+ // Is this a function (i.e. is it something defined in functions.js)?
+ this.isFunction = isFunction;
+ this.token = token;
+}
+
+/**
+ * Checks a result to make sure it has the right type, and throws an
+ * appropriate error otherwise.
+ *
+ * @param {boolean=} consume whether to consume the expected token,
+ * defaults to true
+ */
+Parser.prototype.expect = function(text, consume) {
+ if (this.nextToken.text !== text) {
+ throw new ParseError(
+ "Expected '" + text + "', got '" + this.nextToken.text + "'",
+ this.nextToken
+ );
+ }
+ if (consume !== false) {
+ this.consume();
+ }
+};
+
+/**
+ * Considers the current look ahead token as consumed,
+ * and fetches the one after that as the new look ahead.
+ */
+Parser.prototype.consume = function() {
+ this.nextToken = this.gullet.get(this.mode === "math");
+};
+
+Parser.prototype.switchMode = function(newMode) {
+ this.gullet.unget(this.nextToken);
+ this.mode = newMode;
+ this.consume();
+};
+
+/**
+ * Main parsing function, which parses an entire input.
+ *
+ * @return {?Array.<ParseNode>}
+ */
+Parser.prototype.parse = function() {
+ // Try to parse the input
+ this.mode = "math";
+ this.consume();
+ var parse = this.parseInput();
+ return parse;
+};
+
+/**
+ * Parses an entire input tree.
+ */
+Parser.prototype.parseInput = function() {
+ // Parse an expression
+ var expression = this.parseExpression(false);
+ // If we succeeded, make sure there's an EOF at the end
+ this.expect("EOF", false);
+ return expression;
+};
+
+var endOfExpression = ["}", "\\end", "\\right", "&", "\\\\", "\\cr"];
+
+/**
+ * Parses an "expression", which is a list of atoms.
+ *
+ * @param {boolean} breakOnInfix Should the parsing stop when we hit infix
+ * nodes? This happens when functions have higher precendence
+ * than infix nodes in implicit parses.
+ *
+ * @param {?string} breakOnTokenText The text of the token that the expression
+ * should end with, or `null` if something else should end the
+ * expression.
+ *
+ * @return {ParseNode}
+ */
+Parser.prototype.parseExpression = function(breakOnInfix, breakOnTokenText) {
+ var body = [];
+ // Keep adding atoms to the body until we can't parse any more atoms (either
+ // we reached the end, a }, or a \right)
+ while (true) {
+ var lex = this.nextToken;
+ if (endOfExpression.indexOf(lex.text) !== -1) {
+ break;
+ }
+ if (breakOnTokenText && lex.text === breakOnTokenText) {
+ break;
+ }
+ if (breakOnInfix && functions[lex.text] && functions[lex.text].infix) {
+ break;
+ }
+ var atom = this.parseAtom();
+ if (!atom) {
+ if (!this.settings.throwOnError && lex.text[0] === "\\") {
+ var errorNode = this.handleUnsupportedCmd();
+ body.push(errorNode);
+ continue;
+ }
+
+ break;
+ }
+ body.push(atom);
+ }
+ return this.handleInfixNodes(body);
+};
+
+/**
+ * Rewrites infix operators such as \over with corresponding commands such
+ * as \frac.
+ *
+ * There can only be one infix operator per group. If there's more than one
+ * then the expression is ambiguous. This can be resolved by adding {}.
+ *
+ * @returns {Array}
+ */
+Parser.prototype.handleInfixNodes = function(body) {
+ var overIndex = -1;
+ var funcName;
+
+ for (var i = 0; i < body.length; i++) {
+ var node = body[i];
+ if (node.type === "infix") {
+ if (overIndex !== -1) {
+ throw new ParseError(
+ "only one infix operator per group",
+ node.value.token);
+ }
+ overIndex = i;
+ funcName = node.value.replaceWith;
+ }
+ }
+
+ if (overIndex !== -1) {
+ var numerNode;
+ var denomNode;
+
+ var numerBody = body.slice(0, overIndex);
+ var denomBody = body.slice(overIndex + 1);
+
+ if (numerBody.length === 1 && numerBody[0].type === "ordgroup") {
+ numerNode = numerBody[0];
+ } else {
+ numerNode = new ParseNode("ordgroup", numerBody, this.mode);
+ }
+
+ if (denomBody.length === 1 && denomBody[0].type === "ordgroup") {
+ denomNode = denomBody[0];
+ } else {
+ denomNode = new ParseNode("ordgroup", denomBody, this.mode);
+ }
+
+ var value = this.callFunction(
+ funcName, [numerNode, denomNode], null);
+ return [new ParseNode(value.type, value, this.mode)];
+ } else {
+ return body;
+ }
+};
+
+// The greediness of a superscript or subscript
+var SUPSUB_GREEDINESS = 1;
+
+/**
+ * Handle a subscript or superscript with nice errors.
+ */
+Parser.prototype.handleSupSubscript = function(name) {
+ var symbolToken = this.nextToken;
+ var symbol = symbolToken.text;
+ this.consume();
+ var group = this.parseGroup();
+
+ if (!group) {
+ if (!this.settings.throwOnError && this.nextToken.text[0] === "\\") {
+ return this.handleUnsupportedCmd();
+ } else {
+ throw new ParseError(
+ "Expected group after '" + symbol + "'",
+ symbolToken
+ );
+ }
+ } else if (group.isFunction) {
+ // ^ and _ have a greediness, so handle interactions with functions'
+ // greediness
+ var funcGreediness = functions[group.result].greediness;
+ if (funcGreediness > SUPSUB_GREEDINESS) {
+ return this.parseFunction(group);
+ } else {
+ throw new ParseError(
+ "Got function '" + group.result + "' with no arguments " +
+ "as " + name, symbolToken);
+ }
+ } else {
+ return group.result;
+ }
+};
+
+/**
+ * Converts the textual input of an unsupported command into a text node
+ * contained within a color node whose color is determined by errorColor
+ */
+Parser.prototype.handleUnsupportedCmd = function() {
+ var text = this.nextToken.text;
+ var textordArray = [];
+
+ for (var i = 0; i < text.length; i++) {
+ textordArray.push(new ParseNode("textord", text[i], "text"));
+ }
+
+ var textNode = new ParseNode(
+ "text",
+ {
+ body: textordArray,
+ type: "text",
+ },
+ this.mode);
+
+ var colorNode = new ParseNode(
+ "color",
+ {
+ color: this.settings.errorColor,
+ value: [textNode],
+ type: "color",
+ },
+ this.mode);
+
+ this.consume();
+ return colorNode;
+};
+
+/**
+ * Parses a group with optional super/subscripts.
+ *
+ * @return {?ParseNode}
+ */
+Parser.prototype.parseAtom = function() {
+ // The body of an atom is an implicit group, so that things like
+ // \left(x\right)^2 work correctly.
+ var base = this.parseImplicitGroup();
+
+ // In text mode, we don't have superscripts or subscripts
+ if (this.mode === "text") {
+ return base;
+ }
+
+ // Note that base may be empty (i.e. null) at this point.
+
+ var superscript;
+ var subscript;
+ while (true) {
+ // Lex the first token
+ var lex = this.nextToken;
+
+ if (lex.text === "\\limits" || lex.text === "\\nolimits") {
+ // We got a limit control
+ if (!base || base.type !== "op") {
+ throw new ParseError(
+ "Limit controls must follow a math operator",
+ lex);
+ } else {
+ var limits = lex.text === "\\limits";
+ base.value.limits = limits;
+ base.value.alwaysHandleSupSub = true;
+ }
+ this.consume();
+ } else if (lex.text === "^") {
+ // We got a superscript start
+ if (superscript) {
+ throw new ParseError("Double superscript", lex);
+ }
+ superscript = this.handleSupSubscript("superscript");
+ } else if (lex.text === "_") {
+ // We got a subscript start
+ if (subscript) {
+ throw new ParseError("Double subscript", lex);
+ }
+ subscript = this.handleSupSubscript("subscript");
+ } else if (lex.text === "'") {
+ // We got a prime
+ var prime = new ParseNode("textord", "\\prime", this.mode);
+
+ // Many primes can be grouped together, so we handle this here
+ var primes = [prime];
+ this.consume();
+ // Keep lexing tokens until we get something that's not a prime
+ while (this.nextToken.text === "'") {
+ // For each one, add another prime to the list
+ primes.push(prime);
+ this.consume();
+ }
+ // Put them into an ordgroup as the superscript
+ superscript = new ParseNode("ordgroup", primes, this.mode);
+ } else {
+ // If it wasn't ^, _, or ', stop parsing super/subscripts
+ break;
+ }
+ }
+
+ if (superscript || subscript) {
+ // If we got either a superscript or subscript, create a supsub
+ return new ParseNode("supsub", {
+ base: base,
+ sup: superscript,
+ sub: subscript,
+ }, this.mode);
+ } else {
+ // Otherwise return the original body
+ return base;
+ }
+};
+
+// A list of the size-changing functions, for use in parseImplicitGroup
+var sizeFuncs = [
+ "\\tiny", "\\scriptsize", "\\footnotesize", "\\small", "\\normalsize",
+ "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge",
+];
+
+// A list of the style-changing functions, for use in parseImplicitGroup
+var styleFuncs = [
+ "\\displaystyle", "\\textstyle", "\\scriptstyle", "\\scriptscriptstyle",
+];
+
+/**
+ * Parses an implicit group, which is a group that starts at the end of a
+ * specified, and ends right before a higher explicit group ends, or at EOL. It
+ * is used for functions that appear to affect the current style, like \Large or
+ * \textrm, where instead of keeping a style we just pretend that there is an
+ * implicit grouping after it until the end of the group. E.g.
+ * small text {\Large large text} small text again
+ * It is also used for \left and \right to get the correct grouping.
+ *
+ * @return {?ParseNode}
+ */
+Parser.prototype.parseImplicitGroup = function() {
+ var start = this.parseSymbol();
+
+ if (start == null) {
+ // If we didn't get anything we handle, fall back to parseFunction
+ return this.parseFunction();
+ }
+
+ var func = start.result;
+ var body;
+
+ if (func === "\\left") {
+ // If we see a left:
+ // Parse the entire left function (including the delimiter)
+ var left = this.parseFunction(start);
+ // Parse out the implicit body
+ body = this.parseExpression(false);
+ // Check the next token
+ this.expect("\\right", false);
+ var right = this.parseFunction();
+ return new ParseNode("leftright", {
+ body: body,
+ left: left.value.value,
+ right: right.value.value,
+ }, this.mode);
+ } else if (func === "\\begin") {
+ // begin...end is similar to left...right
+ var begin = this.parseFunction(start);
+ var envName = begin.value.name;
+ if (!environments.hasOwnProperty(envName)) {
+ throw new ParseError(
+ "No such environment: " + envName, begin.value.nameGroup);
+ }
+ // Build the environment object. Arguments and other information will
+ // be made available to the begin and end methods using properties.
+ var env = environments[envName];
+ var args = this.parseArguments("\\begin{" + envName + "}", env);
+ var context = {
+ mode: this.mode,
+ envName: envName,
+ parser: this,
+ positions: args.pop(),
+ };
+ var result = env.handler(context, args);
+ this.expect("\\end", false);
+ var endNameToken = this.nextToken;
+ var end = this.parseFunction();
+ if (end.value.name !== envName) {
+ throw new ParseError(
+ "Mismatch: \\begin{" + envName + "} matched " +
+ "by \\end{" + end.value.name + "}",
+ endNameToken);
+ }
+ result.position = end.position;
+ return result;
+ } else if (utils.contains(sizeFuncs, func)) {
+ // If we see a sizing function, parse out the implict body
+ body = this.parseExpression(false);
+ return new ParseNode("sizing", {
+ // Figure out what size to use based on the list of functions above
+ size: "size" + (utils.indexOf(sizeFuncs, func) + 1),
+ value: body,
+ }, this.mode);
+ } else if (utils.contains(styleFuncs, func)) {
+ // If we see a styling function, parse out the implict body
+ body = this.parseExpression(true);
+ return new ParseNode("styling", {
+ // Figure out what style to use by pulling out the style from
+ // the function name
+ style: func.slice(1, func.length - 5),
+ value: body,
+ }, this.mode);
+ } else {
+ // Defer to parseFunction if it's not a function we handle
+ return this.parseFunction(start);
+ }
+};
+
+/**
+ * Parses an entire function, including its base and all of its arguments.
+ * The base might either have been parsed already, in which case
+ * it is provided as an argument, or it's the next group in the input.
+ *
+ * @param {ParseFuncOrArgument=} baseGroup optional as described above
+ * @return {?ParseNode}
+ */
+Parser.prototype.parseFunction = function(baseGroup) {
+ if (!baseGroup) {
+ baseGroup = this.parseGroup();
+ }
+
+ if (baseGroup) {
+ if (baseGroup.isFunction) {
+ var func = baseGroup.result;
+ var funcData = functions[func];
+ if (this.mode === "text" && !funcData.allowedInText) {
+ throw new ParseError(
+ "Can't use function '" + func + "' in text mode",
+ baseGroup.token);
+ }
+
+ var args = this.parseArguments(func, funcData);
+ var token = baseGroup.token;
+ var result = this.callFunction(func, args, args.pop(), token);
+ return new ParseNode(result.type, result, this.mode);
+ } else {
+ return baseGroup.result;
+ }
+ } else {
+ return null;
+ }
+};
+
+/**
+ * Call a function handler with a suitable context and arguments.
+ */
+Parser.prototype.callFunction = function(name, args, positions, token) {
+ var context = {
+ funcName: name,
+ parser: this,
+ positions: positions,
+ token: token,
+ };
+ return functions[name].handler(context, args);
+};
+
+/**
+ * Parses the arguments of a function or environment
+ *
+ * @param {string} func "\name" or "\begin{name}"
+ * @param {{numArgs:number,numOptionalArgs:number|undefined}} funcData
+ * @return the array of arguments, with the list of positions as last element
+ */
+Parser.prototype.parseArguments = function(func, funcData) {
+ var totalArgs = funcData.numArgs + funcData.numOptionalArgs;
+ if (totalArgs === 0) {
+ return [[this.pos]];
+ }
+
+ var baseGreediness = funcData.greediness;
+ var positions = [this.pos];
+ var args = [];
+
+ for (var i = 0; i < totalArgs; i++) {
+ var nextToken = this.nextToken;
+ var argType = funcData.argTypes && funcData.argTypes[i];
+ var arg;
+ if (i < funcData.numOptionalArgs) {
+ if (argType) {
+ arg = this.parseGroupOfType(argType, true);
+ } else {
+ arg = this.parseGroup(true);
+ }
+ if (!arg) {
+ args.push(null);
+ positions.push(this.pos);
+ continue;
+ }
+ } else {
+ if (argType) {
+ arg = this.parseGroupOfType(argType);
+ } else {
+ arg = this.parseGroup();
+ }
+ if (!arg) {
+ if (!this.settings.throwOnError &&
+ this.nextToken.text[0] === "\\") {
+ arg = new ParseFuncOrArgument(
+ this.handleUnsupportedCmd(this.nextToken.text),
+ false);
+ } else {
+ throw new ParseError(
+ "Expected group after '" + func + "'", nextToken);
+ }
+ }
+ }
+ var argNode;
+ if (arg.isFunction) {
+ var argGreediness =
+ functions[arg.result].greediness;
+ if (argGreediness > baseGreediness) {
+ argNode = this.parseFunction(arg);
+ } else {
+ throw new ParseError(
+ "Got function '" + arg.result + "' as " +
+ "argument to '" + func + "'", nextToken);
+ }
+ } else {
+ argNode = arg.result;
+ }
+ args.push(argNode);
+ positions.push(this.pos);
+ }
+
+ args.push(positions);
+
+ return args;
+};
+
+
+/**
+ * Parses a group when the mode is changing.
+ *
+ * @return {?ParseFuncOrArgument}
+ */
+Parser.prototype.parseGroupOfType = function(innerMode, optional) {
+ var outerMode = this.mode;
+ // Handle `original` argTypes
+ if (innerMode === "original") {
+ innerMode = outerMode;
+ }
+
+ if (innerMode === "color") {
+ return this.parseColorGroup(optional);
+ }
+ if (innerMode === "size") {
+ return this.parseSizeGroup(optional);
+ }
+
+ this.switchMode(innerMode);
+ if (innerMode === "text") {
+ // text mode is special because it should ignore the whitespace before
+ // it
+ while (this.nextToken.text === " ") {
+ this.consume();
+ }
+ }
+ // By the time we get here, innerMode is one of "text" or "math".
+ // We switch the mode of the parser, recurse, then restore the old mode.
+ var res = this.parseGroup(optional);
+ this.switchMode(outerMode);
+ return res;
+};
+
+/**
+ * Parses a group, essentially returning the string formed by the
+ * brace-enclosed tokens plus some position information.
+ *
+ * @param {string} modeName Used to describe the mode in error messages
+ * @param {boolean=} optional Whether the group is optional or required
+ */
+Parser.prototype.parseStringGroup = function(modeName, optional) {
+ if (optional && this.nextToken.text !== "[") {
+ return null;
+ }
+ var outerMode = this.mode;
+ this.mode = "text";
+ this.expect(optional ? "[" : "{");
+ var str = "";
+ var firstToken = this.nextToken;
+ var lastToken = firstToken;
+ while (this.nextToken.text !== (optional ? "]" : "}")) {
+ if (this.nextToken.text === "EOF") {
+ throw new ParseError(
+ "Unexpected end of input in " + modeName,
+ firstToken.range(this.nextToken, str));
+ }
+ lastToken = this.nextToken;
+ str += lastToken.text;
+ this.consume();
+ }
+ this.mode = outerMode;
+ this.expect(optional ? "]" : "}");
+ return firstToken.range(lastToken, str);
+};
+
+/**
+ * Parses a color description.
+ */
+Parser.prototype.parseColorGroup = function(optional) {
+ var res = this.parseStringGroup("color", optional);
+ if (!res) {
+ return null;
+ }
+ var match = (/^(#[a-z0-9]+|[a-z]+)$/i).exec(res.text);
+ if (!match) {
+ throw new ParseError("Invalid color: '" + res.text + "'", res);
+ }
+ return new ParseFuncOrArgument(
+ new ParseNode("color", match[0], this.mode),
+ false);
+};
+
+/**
+ * Parses a size specification, consisting of magnitude and unit.
+ */
+Parser.prototype.parseSizeGroup = function(optional) {
+ var res = this.parseStringGroup("size", optional);
+ if (!res) {
+ return null;
+ }
+ var match = (/(-?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/).exec(res.text);
+ if (!match) {
+ throw new ParseError("Invalid size: '" + res.text + "'", res);
+ }
+ var data = {
+ number: +(match[1] + match[2]), // sign + magnitude, cast to number
+ unit: match[3],
+ };
+ if (data.unit !== "em" && data.unit !== "ex") {
+ throw new ParseError("Invalid unit: '" + data.unit + "'", res);
+ }
+ return new ParseFuncOrArgument(
+ new ParseNode("color", data, this.mode),
+ false);
+};
+
+/**
+ * If the argument is false or absent, this parses an ordinary group,
+ * which is either a single nucleus (like "x") or an expression
+ * in braces (like "{x+y}").
+ * If the argument is true, it parses either a bracket-delimited expression
+ * (like "[x+y]") or returns null to indicate the absence of a
+ * bracket-enclosed group.
+ *
+ * @param {boolean=} optional Whether the group is optional or required
+ * @return {?ParseFuncOrArgument}
+ */
+Parser.prototype.parseGroup = function(optional) {
+ var firstToken = this.nextToken;
+ // Try to parse an open brace
+ if (this.nextToken.text === (optional ? "[" : "{")) {
+ // If we get a brace, parse an expression
+ this.consume();
+ var expression = this.parseExpression(false, optional ? "]" : null);
+ var lastToken = this.nextToken;
+ // Make sure we get a close brace
+ this.expect(optional ? "]" : "}");
+ if (this.mode === "text") {
+ this.formLigatures(expression);
+ }
+ return new ParseFuncOrArgument(
+ new ParseNode("ordgroup", expression, this.mode,
+ firstToken, lastToken),
+ false);
+ } else {
+ // Otherwise, just return a nucleus, or nothing for an optional group
+ return optional ? null : this.parseSymbol();
+ }
+};
+
+/**
+ * Form ligature-like combinations of characters for text mode.
+ * This includes inputs like "--", "---", "``" and "''".
+ * The result will simply replace multiple textord nodes with a single
+ * character in each value by a single textord node having multiple
+ * characters in its value. The representation is still ASCII source.
+ *
+ * @param {Array.<ParseNode>} group the nodes of this group,
+ * list will be moified in place
+ */
+Parser.prototype.formLigatures = function(group) {
+ var i;
+ var n = group.length - 1;
+ for (i = 0; i < n; ++i) {
+ var a = group[i];
+ var v = a.value;
+ if (v === "-" && group[i + 1].value === "-") {
+ if (i + 1 < n && group[i + 2].value === "-") {
+ group.splice(i, 3, new ParseNode(
+ "textord", "---", "text", a, group[i + 2]));
+ n -= 2;
+ } else {
+ group.splice(i, 2, new ParseNode(
+ "textord", "--", "text", a, group[i + 1]));
+ n -= 1;
+ }
+ }
+ if ((v === "'" || v === "`") && group[i + 1].value === v) {
+ group.splice(i, 2, new ParseNode(
+ "textord", v + v, "text", a, group[i + 1]));
+ n -= 1;
+ }
+ }
+};
+
+/**
+ * Parse a single symbol out of the string. Here, we handle both the functions
+ * we have defined, as well as the single character symbols
+ *
+ * @return {?ParseFuncOrArgument}
+ */
+Parser.prototype.parseSymbol = function() {
+ var nucleus = this.nextToken;
+
+ if (functions[nucleus.text]) {
+ this.consume();
+ // If there exists a function with this name, we return the function and
+ // say that it is a function.
+ return new ParseFuncOrArgument(
+ nucleus.text,
+ true, nucleus);
+ } else if (symbols[this.mode][nucleus.text]) {
+ this.consume();
+ // Otherwise if this is a no-argument function, find the type it
+ // corresponds to in the symbols map
+ return new ParseFuncOrArgument(
+ new ParseNode(symbols[this.mode][nucleus.text].group,
+ nucleus.text, this.mode, nucleus),
+ false, nucleus);
+ } else if (this.mode === "text" && cjkRegex.test(nucleus.text)) {
+ this.consume();
+ return new ParseFuncOrArgument(
+ new ParseNode("textord", nucleus.text, this.mode, nucleus),
+ false, nucleus);
+ } else {
+ return null;
+ }
+};
+
+Parser.prototype.ParseNode = ParseNode;
+
+module.exports = Parser;
+
+},{"./MacroExpander":4,"./ParseError":6,"./environments":16,"./functions":19,"./parseData":21,"./symbols":23,"./unicodeRegexes":24,"./utils":25}],8:[function(require,module,exports){
+/**
+ * This is a module for storing settings passed into KaTeX. It correctly handles
+ * default settings.
+ */
+
+/**
+ * Helper function for getting a default value if the value is undefined
+ */
+function get(option, defaultValue) {
+ return option === undefined ? defaultValue : option;
+}
+
+/**
+ * The main Settings object
+ *
+ * The current options stored are:
+ * - displayMode: Whether the expression should be typeset by default in
+ * textstyle or displaystyle (default false)
+ */
+function Settings(options) {
+ // allow null options
+ options = options || {};
+ this.displayMode = get(options.displayMode, false);
+ this.throwOnError = get(options.throwOnError, true);
+ this.errorColor = get(options.errorColor, "#cc0000");
+ this.macros = options.macros || {};
+}
+
+module.exports = Settings;
+
+},{}],9:[function(require,module,exports){
+/**
+ * This file contains information and classes for the various kinds of styles
+ * used in TeX. It provides a generic `Style` class, which holds information
+ * about a specific style. It then provides instances of all the different kinds
+ * of styles possible, and provides functions to move between them and get
+ * information about them.
+ */
+
+/**
+ * The main style class. Contains a unique id for the style, a size (which is
+ * the same for cramped and uncramped version of a style), a cramped flag, and a
+ * size multiplier, which gives the size difference between a style and
+ * textstyle.
+ */
+function Style(id, size, multiplier, cramped) {
+ this.id = id;
+ this.size = size;
+ this.cramped = cramped;
+ this.sizeMultiplier = multiplier;
+}
+
+/**
+ * Get the style of a superscript given a base in the current style.
+ */
+Style.prototype.sup = function() {
+ return styles[sup[this.id]];
+};
+
+/**
+ * Get the style of a subscript given a base in the current style.
+ */
+Style.prototype.sub = function() {
+ return styles[sub[this.id]];
+};
+
+/**
+ * Get the style of a fraction numerator given the fraction in the current
+ * style.
+ */
+Style.prototype.fracNum = function() {
+ return styles[fracNum[this.id]];
+};
+
+/**
+ * Get the style of a fraction denominator given the fraction in the current
+ * style.
+ */
+Style.prototype.fracDen = function() {
+ return styles[fracDen[this.id]];
+};
+
+/**
+ * Get the cramped version of a style (in particular, cramping a cramped style
+ * doesn't change the style).
+ */
+Style.prototype.cramp = function() {
+ return styles[cramp[this.id]];
+};
+
+/**
+ * HTML class name, like "displaystyle cramped"
+ */
+Style.prototype.cls = function() {
+ return sizeNames[this.size] + (this.cramped ? " cramped" : " uncramped");
+};
+
+/**
+ * HTML Reset class name, like "reset-textstyle"
+ */
+Style.prototype.reset = function() {
+ return resetNames[this.size];
+};
+
+// IDs of the different styles
+var D = 0;
+var Dc = 1;
+var T = 2;
+var Tc = 3;
+var S = 4;
+var Sc = 5;
+var SS = 6;
+var SSc = 7;
+
+// String names for the different sizes
+var sizeNames = [
+ "displaystyle textstyle",
+ "textstyle",
+ "scriptstyle",
+ "scriptscriptstyle",
+];
+
+// Reset names for the different sizes
+var resetNames = [
+ "reset-textstyle",
+ "reset-textstyle",
+ "reset-scriptstyle",
+ "reset-scriptscriptstyle",
+];
+
+// Instances of the different styles
+var styles = [
+ new Style(D, 0, 1.0, false),
+ new Style(Dc, 0, 1.0, true),
+ new Style(T, 1, 1.0, false),
+ new Style(Tc, 1, 1.0, true),
+ new Style(S, 2, 0.7, false),
+ new Style(Sc, 2, 0.7, true),
+ new Style(SS, 3, 0.5, false),
+ new Style(SSc, 3, 0.5, true),
+];
+
+// Lookup tables for switching from one style to another
+var sup = [S, Sc, S, Sc, SS, SSc, SS, SSc];
+var sub = [Sc, Sc, Sc, Sc, SSc, SSc, SSc, SSc];
+var fracNum = [T, Tc, S, Sc, SS, SSc, SS, SSc];
+var fracDen = [Tc, Tc, Sc, Sc, SSc, SSc, SSc, SSc];
+var cramp = [Dc, Dc, Tc, Tc, Sc, Sc, SSc, SSc];
+
+// We only export some of the styles. Also, we don't export the `Style` class so
+// no more styles can be generated.
+module.exports = {
+ DISPLAY: styles[D],
+ TEXT: styles[T],
+ SCRIPT: styles[S],
+ SCRIPTSCRIPT: styles[SS],
+};
+
+},{}],10:[function(require,module,exports){
+/* eslint no-console:0 */
+/**
+ * This module contains general functions that can be used for building
+ * different kinds of domTree nodes in a consistent manner.
+ */
+
+var domTree = require("./domTree");
+var fontMetrics = require("./fontMetrics");
+var symbols = require("./symbols");
+var utils = require("./utils");
+
+var greekCapitals = [
+ "\\Gamma",
+ "\\Delta",
+ "\\Theta",
+ "\\Lambda",
+ "\\Xi",
+ "\\Pi",
+ "\\Sigma",
+ "\\Upsilon",
+ "\\Phi",
+ "\\Psi",
+ "\\Omega",
+];
+
+// The following have to be loaded from Main-Italic font, using class mainit
+var mainitLetters = [
+ "\u0131", // dotless i, \imath
+ "\u0237", // dotless j, \jmath
+ "\u00a3", // \pounds
+];
+
+/**
+ * Makes a symbolNode after translation via the list of symbols in symbols.js.
+ * Correctly pulls out metrics for the character, and optionally takes a list of
+ * classes to be attached to the node.
+ */
+var makeSymbol = function(value, style, mode, color, classes) {
+ // Replace the value with its replaced value from symbol.js
+ if (symbols[mode][value] && symbols[mode][value].replace) {
+ value = symbols[mode][value].replace;
+ }
+
+ var metrics = fontMetrics.getCharacterMetrics(value, style);
+
+ var symbolNode;
+ if (metrics) {
+ symbolNode = new domTree.symbolNode(
+ value, metrics.height, metrics.depth, metrics.italic, metrics.skew,
+ classes);
+ } else {
+ // TODO(emily): Figure out a good way to only print this in development
+ typeof console !== "undefined" && console.warn(
+ "No character metrics for '" + value + "' in style '" +
+ style + "'");
+ symbolNode = new domTree.symbolNode(value, 0, 0, 0, 0, classes);
+ }
+
+ if (color) {
+ symbolNode.style.color = color;
+ }
+
+ return symbolNode;
+};
+
+/**
+ * Makes a symbol in Main-Regular or AMS-Regular.
+ * Used for rel, bin, open, close, inner, and punct.
+ */
+var mathsym = function(value, mode, color, classes) {
+ // Decide what font to render the symbol in by its entry in the symbols
+ // table.
+ // Have a special case for when the value = \ because the \ is used as a
+ // textord in unsupported command errors but cannot be parsed as a regular
+ // text ordinal and is therefore not present as a symbol in the symbols
+ // table for text
+ if (value === "\\" || symbols[mode][value].font === "main") {
+ return makeSymbol(value, "Main-Regular", mode, color, classes);
+ } else {
+ return makeSymbol(
+ value, "AMS-Regular", mode, color, classes.concat(["amsrm"]));
+ }
+};
+
+/**
+ * Makes a symbol in the default font for mathords and textords.
+ */
+var mathDefault = function(value, mode, color, classes, type) {
+ if (type === "mathord") {
+ return mathit(value, mode, color, classes);
+ } else if (type === "textord") {
+ return makeSymbol(
+ value, "Main-Regular", mode, color, classes.concat(["mathrm"]));
+ } else {
+ throw new Error("unexpected type: " + type + " in mathDefault");
+ }
+};
+
+/**
+ * Makes a symbol in the italic math font.
+ */
+var mathit = function(value, mode, color, classes) {
+ if (/[0-9]/.test(value.charAt(0)) ||
+ // glyphs for \imath and \jmath do not exist in Math-Italic so we
+ // need to use Main-Italic instead
+ utils.contains(mainitLetters, value) ||
+ utils.contains(greekCapitals, value)) {
+ return makeSymbol(
+ value, "Main-Italic", mode, color, classes.concat(["mainit"]));
+ } else {
+ return makeSymbol(
+ value, "Math-Italic", mode, color, classes.concat(["mathit"]));
+ }
+};
+
+/**
+ * Makes either a mathord or textord in the correct font and color.
+ */
+var makeOrd = function(group, options, type) {
+ var mode = group.mode;
+ var value = group.value;
+ if (symbols[mode][value] && symbols[mode][value].replace) {
+ value = symbols[mode][value].replace;
+ }
+
+ var classes = ["mord"];
+ var color = options.getColor();
+
+ var font = options.font;
+ if (font) {
+ if (font === "mathit" || utils.contains(mainitLetters, value)) {
+ return mathit(value, mode, color, classes);
+ } else {
+ var fontName = fontMap[font].fontName;
+ if (fontMetrics.getCharacterMetrics(value, fontName)) {
+ return makeSymbol(
+ value, fontName, mode, color, classes.concat([font]));
+ } else {
+ return mathDefault(value, mode, color, classes, type);
+ }
+ }
+ } else {
+ return mathDefault(value, mode, color, classes, type);
+ }
+};
+
+/**
+ * Calculate the height, depth, and maxFontSize of an element based on its
+ * children.
+ */
+var sizeElementFromChildren = function(elem) {
+ var height = 0;
+ var depth = 0;
+ var maxFontSize = 0;
+
+ if (elem.children) {
+ for (var i = 0; i < elem.children.length; i++) {
+ if (elem.children[i].height > height) {
+ height = elem.children[i].height;
+ }
+ if (elem.children[i].depth > depth) {
+ depth = elem.children[i].depth;
+ }
+ if (elem.children[i].maxFontSize > maxFontSize) {
+ maxFontSize = elem.children[i].maxFontSize;
+ }
+ }
+ }
+
+ elem.height = height;
+ elem.depth = depth;
+ elem.maxFontSize = maxFontSize;
+};
+
+/**
+ * Makes a span with the given list of classes, list of children, and color.
+ */
+var makeSpan = function(classes, children, color) {
+ var span = new domTree.span(classes, children);
+
+ sizeElementFromChildren(span);
+
+ if (color) {
+ span.style.color = color;
+ }
+
+ return span;
+};
+
+/**
+ * Makes a document fragment with the given list of children.
+ */
+var makeFragment = function(children) {
+ var fragment = new domTree.documentFragment(children);
+
+ sizeElementFromChildren(fragment);
+
+ return fragment;
+};
+
+/**
+ * Makes an element placed in each of the vlist elements to ensure that each
+ * element has the same max font size. To do this, we create a zero-width space
+ * with the correct font size.
+ */
+var makeFontSizer = function(options, fontSize) {
+ var fontSizeInner = makeSpan([], [new domTree.symbolNode("\u200b")]);
+ fontSizeInner.style.fontSize =
+ (fontSize / options.style.sizeMultiplier) + "em";
+
+ var fontSizer = makeSpan(
+ ["fontsize-ensurer", "reset-" + options.size, "size5"],
+ [fontSizeInner]);
+
+ return fontSizer;
+};
+
+/**
+ * Makes a vertical list by stacking elements and kerns on top of each other.
+ * Allows for many different ways of specifying the positioning method.
+ *
+ * Arguments:
+ * - children: A list of child or kern nodes to be stacked on top of each other
+ * (i.e. the first element will be at the bottom, and the last at
+ * the top). Element nodes are specified as
+ * {type: "elem", elem: node}
+ * while kern nodes are specified as
+ * {type: "kern", size: size}
+ * - positionType: The method by which the vlist should be positioned. Valid
+ * values are:
+ * - "individualShift": The children list only contains elem
+ * nodes, and each node contains an extra
+ * "shift" value of how much it should be
+ * shifted (note that shifting is always
+ * moving downwards). positionData is
+ * ignored.
+ * - "top": The positionData specifies the topmost point of
+ * the vlist (note this is expected to be a height,
+ * so positive values move up)
+ * - "bottom": The positionData specifies the bottommost point
+ * of the vlist (note this is expected to be a
+ * depth, so positive values move down
+ * - "shift": The vlist will be positioned such that its
+ * baseline is positionData away from the baseline
+ * of the first child. Positive values move
+ * downwards.
+ * - "firstBaseline": The vlist will be positioned such that
+ * its baseline is aligned with the
+ * baseline of the first child.
+ * positionData is ignored. (this is
+ * equivalent to "shift" with
+ * positionData=0)
+ * - positionData: Data used in different ways depending on positionType
+ * - options: An Options object
+ *
+ */
+var makeVList = function(children, positionType, positionData, options) {
+ var depth;
+ var currPos;
+ var i;
+ if (positionType === "individualShift") {
+ var oldChildren = children;
+ children = [oldChildren[0]];
+
+ // Add in kerns to the list of children to get each element to be
+ // shifted to the correct specified shift
+ depth = -oldChildren[0].shift - oldChildren[0].elem.depth;
+ currPos = depth;
+ for (i = 1; i < oldChildren.length; i++) {
+ var diff = -oldChildren[i].shift - currPos -
+ oldChildren[i].elem.depth;
+ var size = diff -
+ (oldChildren[i - 1].elem.height +
+ oldChildren[i - 1].elem.depth);
+
+ currPos = currPos + diff;
+
+ children.push({type: "kern", size: size});
+ children.push(oldChildren[i]);
+ }
+ } else if (positionType === "top") {
+ // We always start at the bottom, so calculate the bottom by adding up
+ // all the sizes
+ var bottom = positionData;
+ for (i = 0; i < children.length; i++) {
+ if (children[i].type === "kern") {
+ bottom -= children[i].size;
+ } else {
+ bottom -= children[i].elem.height + children[i].elem.depth;
+ }
+ }
+ depth = bottom;
+ } else if (positionType === "bottom") {
+ depth = -positionData;
+ } else if (positionType === "shift") {
+ depth = -children[0].elem.depth - positionData;
+ } else if (positionType === "firstBaseline") {
+ depth = -children[0].elem.depth;
+ } else {
+ depth = 0;
+ }
+
+ // Make the fontSizer
+ var maxFontSize = 0;
+ for (i = 0; i < children.length; i++) {
+ if (children[i].type === "elem") {
+ maxFontSize = Math.max(maxFontSize, children[i].elem.maxFontSize);
+ }
+ }
+ var fontSizer = makeFontSizer(options, maxFontSize);
+
+ // Create a new list of actual children at the correct offsets
+ var realChildren = [];
+ currPos = depth;
+ for (i = 0; i < children.length; i++) {
+ if (children[i].type === "kern") {
+ currPos += children[i].size;
+ } else {
+ var child = children[i].elem;
+
+ var shift = -child.depth - currPos;
+ currPos += child.height + child.depth;
+
+ var childWrap = makeSpan([], [fontSizer, child]);
+ childWrap.height -= shift;
+ childWrap.depth += shift;
+ childWrap.style.top = shift + "em";
+
+ realChildren.push(childWrap);
+ }
+ }
+
+ // Add in an element at the end with no offset to fix the calculation of
+ // baselines in some browsers (namely IE, sometimes safari)
+ var baselineFix = makeSpan(
+ ["baseline-fix"], [fontSizer, new domTree.symbolNode("\u200b")]);
+ realChildren.push(baselineFix);
+
+ var vlist = makeSpan(["vlist"], realChildren);
+ // Fix the final height and depth, in case there were kerns at the ends
+ // since the makeSpan calculation won't take that in to account.
+ vlist.height = Math.max(currPos, vlist.height);
+ vlist.depth = Math.max(-depth, vlist.depth);
+ return vlist;
+};
+
+// A table of size -> font size for the different sizing functions
+var sizingMultiplier = {
+ size1: 0.5,
+ size2: 0.7,
+ size3: 0.8,
+ size4: 0.9,
+ size5: 1.0,
+ size6: 1.2,
+ size7: 1.44,
+ size8: 1.73,
+ size9: 2.07,
+ size10: 2.49,
+};
+
+// A map of spacing functions to their attributes, like size and corresponding
+// CSS class
+var spacingFunctions = {
+ "\\qquad": {
+ size: "2em",
+ className: "qquad",
+ },
+ "\\quad": {
+ size: "1em",
+ className: "quad",
+ },
+ "\\enspace": {
+ size: "0.5em",
+ className: "enspace",
+ },
+ "\\;": {
+ size: "0.277778em",
+ className: "thickspace",
+ },
+ "\\:": {
+ size: "0.22222em",
+ className: "mediumspace",
+ },
+ "\\,": {
+ size: "0.16667em",
+ className: "thinspace",
+ },
+ "\\!": {
+ size: "-0.16667em",
+ className: "negativethinspace",
+ },
+};
+
+/**
+ * Maps TeX font commands to objects containing:
+ * - variant: string used for "mathvariant" attribute in buildMathML.js
+ * - fontName: the "style" parameter to fontMetrics.getCharacterMetrics
+ */
+// A map between tex font commands an MathML mathvariant attribute values
+var fontMap = {
+ // styles
+ "mathbf": {
+ variant: "bold",
+ fontName: "Main-Bold",
+ },
+ "mathrm": {
+ variant: "normal",
+ fontName: "Main-Regular",
+ },
+
+ // "mathit" is missing because it requires the use of two fonts: Main-Italic
+ // and Math-Italic. This is handled by a special case in makeOrd which ends
+ // up calling mathit.
+
+ // families
+ "mathbb": {
+ variant: "double-struck",
+ fontName: "AMS-Regular",
+ },
+ "mathcal": {
+ variant: "script",
+ fontName: "Caligraphic-Regular",
+ },
+ "mathfrak": {
+ variant: "fraktur",
+ fontName: "Fraktur-Regular",
+ },
+ "mathscr": {
+ variant: "script",
+ fontName: "Script-Regular",
+ },
+ "mathsf": {
+ variant: "sans-serif",
+ fontName: "SansSerif-Regular",
+ },
+ "mathtt": {
+ variant: "monospace",
+ fontName: "Typewriter-Regular",
+ },
+};
+
+module.exports = {
+ fontMap: fontMap,
+ makeSymbol: makeSymbol,
+ mathsym: mathsym,
+ makeSpan: makeSpan,
+ makeFragment: makeFragment,
+ makeVList: makeVList,
+ makeOrd: makeOrd,
+ sizingMultiplier: sizingMultiplier,
+ spacingFunctions: spacingFunctions,
+};
+
+},{"./domTree":15,"./fontMetrics":17,"./symbols":23,"./utils":25}],11:[function(require,module,exports){
+/* eslint no-console:0 */
+/**
+ * This file does the main work of building a domTree structure from a parse
+ * tree. The entry point is the `buildHTML` function, which takes a parse tree.
+ * Then, the buildExpression, buildGroup, and various groupTypes functions are
+ * called, to produce a final HTML tree.
+ */
+
+var ParseError = require("./ParseError");
+var Style = require("./Style");
+
+var buildCommon = require("./buildCommon");
+var delimiter = require("./delimiter");
+var domTree = require("./domTree");
+var fontMetrics = require("./fontMetrics");
+var utils = require("./utils");
+
+var makeSpan = buildCommon.makeSpan;
+
+/**
+ * Take a list of nodes, build them in order, and return a list of the built
+ * nodes. This function handles the `prev` node correctly, and passes the
+ * previous element from the list as the prev of the next element.
+ */
+var buildExpression = function(expression, options, prev) {
+ var groups = [];
+ for (var i = 0; i < expression.length; i++) {
+ var group = expression[i];
+ groups.push(buildGroup(group, options, prev));
+ prev = group;
+ }
+ return groups;
+};
+
+// List of types used by getTypeOfGroup,
+// see https://github.com/Khan/KaTeX/wiki/Examining-TeX#group-types
+var groupToType = {
+ mathord: "mord",
+ textord: "mord",
+ bin: "mbin",
+ rel: "mrel",
+ text: "mord",
+ open: "mopen",
+ close: "mclose",
+ inner: "minner",
+ genfrac: "mord",
+ array: "mord",
+ spacing: "mord",
+ punct: "mpunct",
+ ordgroup: "mord",
+ op: "mop",
+ katex: "mord",
+ overline: "mord",
+ underline: "mord",
+ rule: "mord",
+ leftright: "minner",
+ sqrt: "mord",
+ accent: "mord",
+};
+
+/**
+ * Gets the final math type of an expression, given its group type. This type is
+ * used to determine spacing between elements, and affects bin elements by
+ * causing them to change depending on what types are around them. This type
+ * must be attached to the outermost node of an element as a CSS class so that
+ * spacing with its surrounding elements works correctly.
+ *
+ * Some elements can be mapped one-to-one from group type to math type, and
+ * those are listed in the `groupToType` table.
+ *
+ * Others (usually elements that wrap around other elements) often have
+ * recursive definitions, and thus call `getTypeOfGroup` on their inner
+ * elements.
+ */
+var getTypeOfGroup = function(group) {
+ if (group == null) {
+ // Like when typesetting $^3$
+ return groupToType.mathord;
+ } else if (group.type === "supsub") {
+ return getTypeOfGroup(group.value.base);
+ } else if (group.type === "llap" || group.type === "rlap") {
+ return getTypeOfGroup(group.value);
+ } else if (group.type === "color") {
+ return getTypeOfGroup(group.value.value);
+ } else if (group.type === "sizing") {
+ return getTypeOfGroup(group.value.value);
+ } else if (group.type === "styling") {
+ return getTypeOfGroup(group.value.value);
+ } else if (group.type === "delimsizing") {
+ return groupToType[group.value.delimType];
+ } else {
+ return groupToType[group.type];
+ }
+};
+
+/**
+ * Sometimes, groups perform special rules when they have superscripts or
+ * subscripts attached to them. This function lets the `supsub` group know that
+ * its inner element should handle the superscripts and subscripts instead of
+ * handling them itself.
+ */
+var shouldHandleSupSub = function(group, options) {
+ if (!group) {
+ return false;
+ } else if (group.type === "op") {
+ // Operators handle supsubs differently when they have limits
+ // (e.g. `\displaystyle\sum_2^3`)
+ return group.value.limits &&
+ (options.style.size === Style.DISPLAY.size ||
+ group.value.alwaysHandleSupSub);
+ } else if (group.type === "accent") {
+ return isCharacterBox(group.value.base);
+ } else {
+ return null;
+ }
+};
+
+/**
+ * Sometimes we want to pull out the innermost element of a group. In most
+ * cases, this will just be the group itself, but when ordgroups and colors have
+ * a single element, we want to pull that out.
+ */
+var getBaseElem = function(group) {
+ if (!group) {
+ return false;
+ } else if (group.type === "ordgroup") {
+ if (group.value.length === 1) {
+ return getBaseElem(group.value[0]);
+ } else {
+ return group;
+ }
+ } else if (group.type === "color") {
+ if (group.value.value.length === 1) {
+ return getBaseElem(group.value.value[0]);
+ } else {
+ return group;
+ }
+ } else if (group.type === "font") {
+ return getBaseElem(group.value.body);
+ } else {
+ return group;
+ }
+};
+
+/**
+ * TeXbook algorithms often reference "character boxes", which are simply groups
+ * with a single character in them. To decide if something is a character box,
+ * we find its innermost group, and see if it is a single character.
+ */
+var isCharacterBox = function(group) {
+ var baseElem = getBaseElem(group);
+
+ // These are all they types of groups which hold single characters
+ return baseElem.type === "mathord" ||
+ baseElem.type === "textord" ||
+ baseElem.type === "bin" ||
+ baseElem.type === "rel" ||
+ baseElem.type === "inner" ||
+ baseElem.type === "open" ||
+ baseElem.type === "close" ||
+ baseElem.type === "punct";
+};
+
+var makeNullDelimiter = function(options) {
+ return makeSpan([
+ "sizing", "reset-" + options.size, "size5",
+ options.style.reset(), Style.TEXT.cls(),
+ "nulldelimiter",
+ ]);
+};
+
+/**
+ * This is a map of group types to the function used to handle that type.
+ * Simpler types come at the beginning, while complicated types come afterwards.
+ */
+var groupTypes = {};
+
+groupTypes.mathord = function(group, options, prev) {
+ return buildCommon.makeOrd(group, options, "mathord");
+};
+
+groupTypes.textord = function(group, options, prev) {
+ return buildCommon.makeOrd(group, options, "textord");
+};
+
+groupTypes.bin = function(group, options, prev) {
+ var className = "mbin";
+ // Pull out the most recent element. Do some special handling to find
+ // things at the end of a \color group. Note that we don't use the same
+ // logic for ordgroups (which count as ords).
+ var prevAtom = prev;
+ while (prevAtom && prevAtom.type === "color") {
+ var atoms = prevAtom.value.value;
+ prevAtom = atoms[atoms.length - 1];
+ }
+ // See TeXbook pg. 442-446, Rules 5 and 6, and the text before Rule 19.
+ // Here, we determine whether the bin should turn into an ord. We
+ // currently only apply Rule 5.
+ if (!prev || utils.contains(["mbin", "mopen", "mrel", "mop", "mpunct"],
+ getTypeOfGroup(prevAtom))) {
+ group.type = "textord";
+ className = "mord";
+ }
+
+ return buildCommon.mathsym(
+ group.value, group.mode, options.getColor(), [className]);
+};
+
+groupTypes.rel = function(group, options, prev) {
+ return buildCommon.mathsym(
+ group.value, group.mode, options.getColor(), ["mrel"]);
+};
+
+groupTypes.open = function(group, options, prev) {
+ return buildCommon.mathsym(
+ group.value, group.mode, options.getColor(), ["mopen"]);
+};
+
+groupTypes.close = function(group, options, prev) {
+ return buildCommon.mathsym(
+ group.value, group.mode, options.getColor(), ["mclose"]);
+};
+
+groupTypes.inner = function(group, options, prev) {
+ return buildCommon.mathsym(
+ group.value, group.mode, options.getColor(), ["minner"]);
+};
+
+groupTypes.punct = function(group, options, prev) {
+ return buildCommon.mathsym(
+ group.value, group.mode, options.getColor(), ["mpunct"]);
+};
+
+groupTypes.ordgroup = function(group, options, prev) {
+ return makeSpan(
+ ["mord", options.style.cls()],
+ buildExpression(group.value, options.reset())
+ );
+};
+
+groupTypes.text = function(group, options, prev) {
+ return makeSpan(["text", "mord", options.style.cls()],
+ buildExpression(group.value.body, options.reset()));
+};
+
+groupTypes.color = function(group, options, prev) {
+ var elements = buildExpression(
+ group.value.value,
+ options.withColor(group.value.color),
+ prev
+ );
+
+ // \color isn't supposed to affect the type of the elements it contains.
+ // To accomplish this, we wrap the results in a fragment, so the inner
+ // elements will be able to directly interact with their neighbors. For
+ // example, `\color{red}{2 +} 3` has the same spacing as `2 + 3`
+ return new buildCommon.makeFragment(elements);
+};
+
+groupTypes.supsub = function(group, options, prev) {
+ // Superscript and subscripts are handled in the TeXbook on page
+ // 445-446, rules 18(a-f).
+
+ // Here is where we defer to the inner group if it should handle
+ // superscripts and subscripts itself.
+ if (shouldHandleSupSub(group.value.base, options)) {
+ return groupTypes[group.value.base.type](group, options, prev);
+ }
+
+ var base = buildGroup(group.value.base, options.reset());
+ var supmid;
+ var submid;
+ var sup;
+ var sub;
+
+ if (group.value.sup) {
+ sup = buildGroup(group.value.sup,
+ options.withStyle(options.style.sup()));
+ supmid = makeSpan(
+ [options.style.reset(), options.style.sup().cls()], [sup]);
+ }
+
+ if (group.value.sub) {
+ sub = buildGroup(group.value.sub,
+ options.withStyle(options.style.sub()));
+ submid = makeSpan(
+ [options.style.reset(), options.style.sub().cls()], [sub]);
+ }
+
+ // Rule 18a
+ var supShift;
+ var subShift;
+ if (isCharacterBox(group.value.base)) {
+ supShift = 0;
+ subShift = 0;
+ } else {
+ supShift = base.height - fontMetrics.metrics.supDrop;
+ subShift = base.depth + fontMetrics.metrics.subDrop;
+ }
+
+ // Rule 18c
+ var minSupShift;
+ if (options.style === Style.DISPLAY) {
+ minSupShift = fontMetrics.metrics.sup1;
+ } else if (options.style.cramped) {
+ minSupShift = fontMetrics.metrics.sup3;
+ } else {
+ minSupShift = fontMetrics.metrics.sup2;
+ }
+
+ // scriptspace is a font-size-independent size, so scale it
+ // appropriately
+ var multiplier = Style.TEXT.sizeMultiplier *
+ options.style.sizeMultiplier;
+ var scriptspace =
+ (0.5 / fontMetrics.metrics.ptPerEm) / multiplier + "em";
+
+ var supsub;
+ if (!group.value.sup) {
+ // Rule 18b
+ subShift = Math.max(
+ subShift, fontMetrics.metrics.sub1,
+ sub.height - 0.8 * fontMetrics.metrics.xHeight);
+
+ supsub = buildCommon.makeVList([
+ {type: "elem", elem: submid},
+ ], "shift", subShift, options);
+
+ supsub.children[0].style.marginRight = scriptspace;
+
+ // Subscripts shouldn't be shifted by the base's italic correction.
+ // Account for that by shifting the subscript back the appropriate
+ // amount. Note we only do this when the base is a single symbol.
+ if (base instanceof domTree.symbolNode) {
+ supsub.children[0].style.marginLeft = -base.italic + "em";
+ }
+ } else if (!group.value.sub) {
+ // Rule 18c, d
+ supShift = Math.max(supShift, minSupShift,
+ sup.depth + 0.25 * fontMetrics.metrics.xHeight);
+
+ supsub = buildCommon.makeVList([
+ {type: "elem", elem: supmid},
+ ], "shift", -supShift, options);
+
+ supsub.children[0].style.marginRight = scriptspace;
+ } else {
+ supShift = Math.max(
+ supShift, minSupShift,
+ sup.depth + 0.25 * fontMetrics.metrics.xHeight);
+ subShift = Math.max(subShift, fontMetrics.metrics.sub2);
+
+ var ruleWidth = fontMetrics.metrics.defaultRuleThickness;
+
+ // Rule 18e
+ if ((supShift - sup.depth) - (sub.height - subShift) <
+ 4 * ruleWidth) {
+ subShift = 4 * ruleWidth - (supShift - sup.depth) + sub.height;
+ var psi = 0.8 * fontMetrics.metrics.xHeight -
+ (supShift - sup.depth);
+ if (psi > 0) {
+ supShift += psi;
+ subShift -= psi;
+ }
+ }
+
+ supsub = buildCommon.makeVList([
+ {type: "elem", elem: submid, shift: subShift},
+ {type: "elem", elem: supmid, shift: -supShift},
+ ], "individualShift", null, options);
+
+ // See comment above about subscripts not being shifted
+ if (base instanceof domTree.symbolNode) {
+ supsub.children[0].style.marginLeft = -base.italic + "em";
+ }
+
+ supsub.children[0].style.marginRight = scriptspace;
+ supsub.children[1].style.marginRight = scriptspace;
+ }
+
+ // We ensure to wrap the supsub vlist in a span.msupsub to reset text-align
+ return makeSpan([getTypeOfGroup(group.value.base)],
+ [base, makeSpan(["msupsub"], [supsub])]);
+};
+
+groupTypes.genfrac = function(group, options, prev) {
+ // Fractions are handled in the TeXbook on pages 444-445, rules 15(a-e).
+ // Figure out what style this fraction should be in based on the
+ // function used
+ var fstyle = options.style;
+ if (group.value.size === "display") {
+ fstyle = Style.DISPLAY;
+ } else if (group.value.size === "text") {
+ fstyle = Style.TEXT;
+ }
+
+ var nstyle = fstyle.fracNum();
+ var dstyle = fstyle.fracDen();
+
+ var numer = buildGroup(group.value.numer, options.withStyle(nstyle));
+ var numerreset = makeSpan([fstyle.reset(), nstyle.cls()], [numer]);
+
+ var denom = buildGroup(group.value.denom, options.withStyle(dstyle));
+ var denomreset = makeSpan([fstyle.reset(), dstyle.cls()], [denom]);
+
+ var ruleWidth;
+ if (group.value.hasBarLine) {
+ ruleWidth = fontMetrics.metrics.defaultRuleThickness /
+ options.style.sizeMultiplier;
+ } else {
+ ruleWidth = 0;
+ }
+
+ // Rule 15b
+ var numShift;
+ var clearance;
+ var denomShift;
+ if (fstyle.size === Style.DISPLAY.size) {
+ numShift = fontMetrics.metrics.num1;
+ if (ruleWidth > 0) {
+ clearance = 3 * ruleWidth;
+ } else {
+ clearance = 7 * fontMetrics.metrics.defaultRuleThickness;
+ }
+ denomShift = fontMetrics.metrics.denom1;
+ } else {
+ if (ruleWidth > 0) {
+ numShift = fontMetrics.metrics.num2;
+ clearance = ruleWidth;
+ } else {
+ numShift = fontMetrics.metrics.num3;
+ clearance = 3 * fontMetrics.metrics.defaultRuleThickness;
+ }
+ denomShift = fontMetrics.metrics.denom2;
+ }
+
+ var frac;
+ if (ruleWidth === 0) {
+ // Rule 15c
+ var candiateClearance =
+ (numShift - numer.depth) - (denom.height - denomShift);
+ if (candiateClearance < clearance) {
+ numShift += 0.5 * (clearance - candiateClearance);
+ denomShift += 0.5 * (clearance - candiateClearance);
+ }
+
+ frac = buildCommon.makeVList([
+ {type: "elem", elem: denomreset, shift: denomShift},
+ {type: "elem", elem: numerreset, shift: -numShift},
+ ], "individualShift", null, options);
+ } else {
+ // Rule 15d
+ var axisHeight = fontMetrics.metrics.axisHeight;
+
+ if ((numShift - numer.depth) - (axisHeight + 0.5 * ruleWidth) <
+ clearance) {
+ numShift +=
+ clearance - ((numShift - numer.depth) -
+ (axisHeight + 0.5 * ruleWidth));
+ }
+
+ if ((axisHeight - 0.5 * ruleWidth) - (denom.height - denomShift) <
+ clearance) {
+ denomShift +=
+ clearance - ((axisHeight - 0.5 * ruleWidth) -
+ (denom.height - denomShift));
+ }
+
+ var mid = makeSpan(
+ [options.style.reset(), Style.TEXT.cls(), "frac-line"]);
+ // Manually set the height of the line because its height is
+ // created in CSS
+ mid.height = ruleWidth;
+
+ var midShift = -(axisHeight - 0.5 * ruleWidth);
+
+ frac = buildCommon.makeVList([
+ {type: "elem", elem: denomreset, shift: denomShift},
+ {type: "elem", elem: mid, shift: midShift},
+ {type: "elem", elem: numerreset, shift: -numShift},
+ ], "individualShift", null, options);
+ }
+
+ // Since we manually change the style sometimes (with \dfrac or \tfrac),
+ // account for the possible size change here.
+ frac.height *= fstyle.sizeMultiplier / options.style.sizeMultiplier;
+ frac.depth *= fstyle.sizeMultiplier / options.style.sizeMultiplier;
+
+ // Rule 15e
+ var delimSize;
+ if (fstyle.size === Style.DISPLAY.size) {
+ delimSize = fontMetrics.metrics.delim1;
+ } else {
+ delimSize = fontMetrics.metrics.getDelim2(fstyle);
+ }
+
+ var leftDelim;
+ var rightDelim;
+ if (group.value.leftDelim == null) {
+ leftDelim = makeNullDelimiter(options);
+ } else {
+ leftDelim = delimiter.customSizedDelim(
+ group.value.leftDelim, delimSize, true,
+ options.withStyle(fstyle), group.mode);
+ }
+ if (group.value.rightDelim == null) {
+ rightDelim = makeNullDelimiter(options);
+ } else {
+ rightDelim = delimiter.customSizedDelim(
+ group.value.rightDelim, delimSize, true,
+ options.withStyle(fstyle), group.mode);
+ }
+
+ return makeSpan(
+ ["mord", options.style.reset(), fstyle.cls()],
+ [leftDelim, makeSpan(["mfrac"], [frac]), rightDelim],
+ options.getColor());
+};
+
+groupTypes.array = function(group, options, prev) {
+ var r;
+ var c;
+ var nr = group.value.body.length;
+ var nc = 0;
+ var body = new Array(nr);
+
+ // Horizontal spacing
+ var pt = 1 / fontMetrics.metrics.ptPerEm;
+ var arraycolsep = 5 * pt; // \arraycolsep in article.cls
+
+ // Vertical spacing
+ var baselineskip = 12 * pt; // see size10.clo
+ // Default \arraystretch from lttab.dtx
+ // TODO(gagern): may get redefined once we have user-defined macros
+ var arraystretch = utils.deflt(group.value.arraystretch, 1);
+ var arrayskip = arraystretch * baselineskip;
+ var arstrutHeight = 0.7 * arrayskip; // \strutbox in ltfsstrc.dtx and
+ var arstrutDepth = 0.3 * arrayskip; // \@arstrutbox in lttab.dtx
+
+ var totalHeight = 0;
+ for (r = 0; r < group.value.body.length; ++r) {
+ var inrow = group.value.body[r];
+ var height = arstrutHeight; // \@array adds an \@arstrut
+ var depth = arstrutDepth; // to each tow (via the template)
+
+ if (nc < inrow.length) {
+ nc = inrow.length;
+ }
+
+ var outrow = new Array(inrow.length);
+ for (c = 0; c < inrow.length; ++c) {
+ var elt = buildGroup(inrow[c], options);
+ if (depth < elt.depth) {
+ depth = elt.depth;
+ }
+ if (height < elt.height) {
+ height = elt.height;
+ }
+ outrow[c] = elt;
+ }
+
+ var gap = 0;
+ if (group.value.rowGaps[r]) {
+ gap = group.value.rowGaps[r].value;
+ switch (gap.unit) {
+ case "em":
+ gap = gap.number;
+ break;
+ case "ex":
+ gap = gap.number * fontMetrics.metrics.emPerEx;
+ break;
+ default:
+ console.error("Can't handle unit " + gap.unit);
+ gap = 0;
+ }
+ if (gap > 0) { // \@argarraycr
+ gap += arstrutDepth;
+ if (depth < gap) {
+ depth = gap; // \@xargarraycr
+ }
+ gap = 0;
+ }
+ }
+
+ outrow.height = height;
+ outrow.depth = depth;
+ totalHeight += height;
+ outrow.pos = totalHeight;
+ totalHeight += depth + gap; // \@yargarraycr
+ body[r] = outrow;
+ }
+
+ var offset = totalHeight / 2 + fontMetrics.metrics.axisHeight;
+ var colDescriptions = group.value.cols || [];
+ var cols = [];
+ var colSep;
+ var colDescrNum;
+ for (c = 0, colDescrNum = 0;
+ // Continue while either there are more columns or more column
+ // descriptions, so trailing separators don't get lost.
+ c < nc || colDescrNum < colDescriptions.length;
+ ++c, ++colDescrNum) {
+
+ var colDescr = colDescriptions[colDescrNum] || {};
+
+ var firstSeparator = true;
+ while (colDescr.type === "separator") {
+ // If there is more than one separator in a row, add a space
+ // between them.
+ if (!firstSeparator) {
+ colSep = makeSpan(["arraycolsep"], []);
+ colSep.style.width =
+ fontMetrics.metrics.doubleRuleSep + "em";
+ cols.push(colSep);
+ }
+
+ if (colDescr.separator === "|") {
+ var separator = makeSpan(
+ ["vertical-separator"],
+ []);
+ separator.style.height = totalHeight + "em";
+ separator.style.verticalAlign =
+ -(totalHeight - offset) + "em";
+
+ cols.push(separator);
+ } else {
+ throw new ParseError(
+ "Invalid separator type: " + colDescr.separator);
+ }
+
+ colDescrNum++;
+ colDescr = colDescriptions[colDescrNum] || {};
+ firstSeparator = false;
+ }
+
+ if (c >= nc) {
+ continue;
+ }
+
+ var sepwidth;
+ if (c > 0 || group.value.hskipBeforeAndAfter) {
+ sepwidth = utils.deflt(colDescr.pregap, arraycolsep);
+ if (sepwidth !== 0) {
+ colSep = makeSpan(["arraycolsep"], []);
+ colSep.style.width = sepwidth + "em";
+ cols.push(colSep);
+ }
+ }
+
+ var col = [];
+ for (r = 0; r < nr; ++r) {
+ var row = body[r];
+ var elem = row[c];
+ if (!elem) {
+ continue;
+ }
+ var shift = row.pos - offset;
+ elem.depth = row.depth;
+ elem.height = row.height;
+ col.push({type: "elem", elem: elem, shift: shift});
+ }
+
+ col = buildCommon.makeVList(col, "individualShift", null, options);
+ col = makeSpan(
+ ["col-align-" + (colDescr.align || "c")],
+ [col]);
+ cols.push(col);
+
+ if (c < nc - 1 || group.value.hskipBeforeAndAfter) {
+ sepwidth = utils.deflt(colDescr.postgap, arraycolsep);
+ if (sepwidth !== 0) {
+ colSep = makeSpan(["arraycolsep"], []);
+ colSep.style.width = sepwidth + "em";
+ cols.push(colSep);
+ }
+ }
+ }
+ body = makeSpan(["mtable"], cols);
+ return makeSpan(["mord"], [body], options.getColor());
+};
+
+groupTypes.spacing = function(group, options, prev) {
+ if (group.value === "\\ " || group.value === "\\space" ||
+ group.value === " " || group.value === "~") {
+ // Spaces are generated by adding an actual space. Each of these
+ // things has an entry in the symbols table, so these will be turned
+ // into appropriate outputs.
+ return makeSpan(
+ ["mord", "mspace"],
+ [buildCommon.mathsym(group.value, group.mode)]
+ );
+ } else {
+ // Other kinds of spaces are of arbitrary width. We use CSS to
+ // generate these.
+ return makeSpan(
+ ["mord", "mspace",
+ buildCommon.spacingFunctions[group.value].className]);
+ }
+};
+
+groupTypes.llap = function(group, options, prev) {
+ var inner = makeSpan(
+ ["inner"], [buildGroup(group.value.body, options.reset())]);
+ var fix = makeSpan(["fix"], []);
+ return makeSpan(
+ ["llap", options.style.cls()], [inner, fix]);
+};
+
+groupTypes.rlap = function(group, options, prev) {
+ var inner = makeSpan(
+ ["inner"], [buildGroup(group.value.body, options.reset())]);
+ var fix = makeSpan(["fix"], []);
+ return makeSpan(
+ ["rlap", options.style.cls()], [inner, fix]);
+};
+
+groupTypes.op = function(group, options, prev) {
+ // Operators are handled in the TeXbook pg. 443-444, rule 13(a).
+ var supGroup;
+ var subGroup;
+ var hasLimits = false;
+ if (group.type === "supsub" ) {
+ // If we have limits, supsub will pass us its group to handle. Pull
+ // out the superscript and subscript and set the group to the op in
+ // its base.
+ supGroup = group.value.sup;
+ subGroup = group.value.sub;
+ group = group.value.base;
+ hasLimits = true;
+ }
+
+ // Most operators have a large successor symbol, but these don't.
+ var noSuccessor = [
+ "\\smallint",
+ ];
+
+ var large = false;
+ if (options.style.size === Style.DISPLAY.size &&
+ group.value.symbol &&
+ !utils.contains(noSuccessor, group.value.body)) {
+
+ // Most symbol operators get larger in displaystyle (rule 13)
+ large = true;
+ }
+
+ var base;
+ var baseShift = 0;
+ var slant = 0;
+ if (group.value.symbol) {
+ // If this is a symbol, create the symbol.
+ var style = large ? "Size2-Regular" : "Size1-Regular";
+ base = buildCommon.makeSymbol(
+ group.value.body, style, "math", options.getColor(),
+ ["op-symbol", large ? "large-op" : "small-op", "mop"]);
+
+ // Shift the symbol so its center lies on the axis (rule 13). It
+ // appears that our fonts have the centers of the symbols already
+ // almost on the axis, so these numbers are very small. Note we
+ // don't actually apply this here, but instead it is used either in
+ // the vlist creation or separately when there are no limits.
+ baseShift = (base.height - base.depth) / 2 -
+ fontMetrics.metrics.axisHeight *
+ options.style.sizeMultiplier;
+
+ // The slant of the symbol is just its italic correction.
+ slant = base.italic;
+ } else {
+ // Otherwise, this is a text operator. Build the text from the
+ // operator's name.
+ // TODO(emily): Add a space in the middle of some of these
+ // operators, like \limsup
+ var output = [];
+ for (var i = 1; i < group.value.body.length; i++) {
+ output.push(buildCommon.mathsym(group.value.body[i], group.mode));
+ }
+ base = makeSpan(["mop"], output, options.getColor());
+ }
+
+ if (hasLimits) {
+ // IE 8 clips \int if it is in a display: inline-block. We wrap it
+ // in a new span so it is an inline, and works.
+ base = makeSpan([], [base]);
+
+ var supmid;
+ var supKern;
+ var submid;
+ var subKern;
+ // We manually have to handle the superscripts and subscripts. This,
+ // aside from the kern calculations, is copied from supsub.
+ if (supGroup) {
+ var sup = buildGroup(
+ supGroup, options.withStyle(options.style.sup()));
+ supmid = makeSpan(
+ [options.style.reset(), options.style.sup().cls()], [sup]);
+
+ supKern = Math.max(
+ fontMetrics.metrics.bigOpSpacing1,
+ fontMetrics.metrics.bigOpSpacing3 - sup.depth);
+ }
+
+ if (subGroup) {
+ var sub = buildGroup(
+ subGroup, options.withStyle(options.style.sub()));
+ submid = makeSpan(
+ [options.style.reset(), options.style.sub().cls()],
+ [sub]);
+
+ subKern = Math.max(
+ fontMetrics.metrics.bigOpSpacing2,
+ fontMetrics.metrics.bigOpSpacing4 - sub.height);
+ }
+
+ // Build the final group as a vlist of the possible subscript, base,
+ // and possible superscript.
+ var finalGroup;
+ var top;
+ var bottom;
+ if (!supGroup) {
+ top = base.height - baseShift;
+
+ finalGroup = buildCommon.makeVList([
+ {type: "kern", size: fontMetrics.metrics.bigOpSpacing5},
+ {type: "elem", elem: submid},
+ {type: "kern", size: subKern},
+ {type: "elem", elem: base},
+ ], "top", top, options);
+
+ // Here, we shift the limits by the slant of the symbol. Note
+ // that we are supposed to shift the limits by 1/2 of the slant,
+ // but since we are centering the limits adding a full slant of
+ // margin will shift by 1/2 that.
+ finalGroup.children[0].style.marginLeft = -slant + "em";
+ } else if (!subGroup) {
+ bottom = base.depth + baseShift;
+
+ finalGroup = buildCommon.makeVList([
+ {type: "elem", elem: base},
+ {type: "kern", size: supKern},
+ {type: "elem", elem: supmid},
+ {type: "kern", size: fontMetrics.metrics.bigOpSpacing5},
+ ], "bottom", bottom, options);
+
+ // See comment above about slants
+ finalGroup.children[1].style.marginLeft = slant + "em";
+ } else if (!supGroup && !subGroup) {
+ // This case probably shouldn't occur (this would mean the
+ // supsub was sending us a group with no superscript or
+ // subscript) but be safe.
+ return base;
+ } else {
+ bottom = fontMetrics.metrics.bigOpSpacing5 +
+ submid.height + submid.depth +
+ subKern +
+ base.depth + baseShift;
+
+ finalGroup = buildCommon.makeVList([
+ {type: "kern", size: fontMetrics.metrics.bigOpSpacing5},
+ {type: "elem", elem: submid},
+ {type: "kern", size: subKern},
+ {type: "elem", elem: base},
+ {type: "kern", size: supKern},
+ {type: "elem", elem: supmid},
+ {type: "kern", size: fontMetrics.metrics.bigOpSpacing5},
+ ], "bottom", bottom, options);
+
+ // See comment above about slants
+ finalGroup.children[0].style.marginLeft = -slant + "em";
+ finalGroup.children[2].style.marginLeft = slant + "em";
+ }
+
+ return makeSpan(["mop", "op-limits"], [finalGroup]);
+ } else {
+ if (group.value.symbol) {
+ base.style.top = baseShift + "em";
+ }
+
+ return base;
+ }
+};
+
+groupTypes.katex = function(group, options, prev) {
+ // The KaTeX logo. The offsets for the K and a were chosen to look
+ // good, but the offsets for the T, E, and X were taken from the
+ // definition of \TeX in TeX (see TeXbook pg. 356)
+ var k = makeSpan(
+ ["k"], [buildCommon.mathsym("K", group.mode)]);
+ var a = makeSpan(
+ ["a"], [buildCommon.mathsym("A", group.mode)]);
+
+ a.height = (a.height + 0.2) * 0.75;
+ a.depth = (a.height - 0.2) * 0.75;
+
+ var t = makeSpan(
+ ["t"], [buildCommon.mathsym("T", group.mode)]);
+ var e = makeSpan(
+ ["e"], [buildCommon.mathsym("E", group.mode)]);
+
+ e.height = (e.height - 0.2155);
+ e.depth = (e.depth + 0.2155);
+
+ var x = makeSpan(
+ ["x"], [buildCommon.mathsym("X", group.mode)]);
+
+ return makeSpan(
+ ["katex-logo", "mord"], [k, a, t, e, x], options.getColor());
+};
+
+groupTypes.overline = function(group, options, prev) {
+ // Overlines are handled in the TeXbook pg 443, Rule 9.
+
+ // Build the inner group in the cramped style.
+ var innerGroup = buildGroup(group.value.body,
+ options.withStyle(options.style.cramp()));
+
+ var ruleWidth = fontMetrics.metrics.defaultRuleThickness /
+ options.style.sizeMultiplier;
+
+ // Create the line above the body
+ var line = makeSpan(
+ [options.style.reset(), Style.TEXT.cls(), "overline-line"]);
+ line.height = ruleWidth;
+ line.maxFontSize = 1.0;
+
+ // Generate the vlist, with the appropriate kerns
+ var vlist = buildCommon.makeVList([
+ {type: "elem", elem: innerGroup},
+ {type: "kern", size: 3 * ruleWidth},
+ {type: "elem", elem: line},
+ {type: "kern", size: ruleWidth},
+ ], "firstBaseline", null, options);
+
+ return makeSpan(["overline", "mord"], [vlist], options.getColor());
+};
+
+groupTypes.underline = function(group, options, prev) {
+ // Underlines are handled in the TeXbook pg 443, Rule 10.
+
+ // Build the inner group.
+ var innerGroup = buildGroup(group.value.body, options);
+
+ var ruleWidth = fontMetrics.metrics.defaultRuleThickness /
+ options.style.sizeMultiplier;
+
+ // Create the line above the body
+ var line = makeSpan(
+ [options.style.reset(), Style.TEXT.cls(), "underline-line"]);
+ line.height = ruleWidth;
+ line.maxFontSize = 1.0;
+
+ // Generate the vlist, with the appropriate kerns
+ var vlist = buildCommon.makeVList([
+ {type: "kern", size: ruleWidth},
+ {type: "elem", elem: line},
+ {type: "kern", size: 3 * ruleWidth},
+ {type: "elem", elem: innerGroup},
+ ], "top", innerGroup.height, options);
+
+ return makeSpan(["underline", "mord"], [vlist], options.getColor());
+};
+
+groupTypes.sqrt = function(group, options, prev) {
+ // Square roots are handled in the TeXbook pg. 443, Rule 11.
+
+ // First, we do the same steps as in overline to build the inner group
+ // and line
+ var inner = buildGroup(group.value.body,
+ options.withStyle(options.style.cramp()));
+
+ var ruleWidth = fontMetrics.metrics.defaultRuleThickness /
+ options.style.sizeMultiplier;
+
+ var line = makeSpan(
+ [options.style.reset(), Style.TEXT.cls(), "sqrt-line"], [],
+ options.getColor());
+ line.height = ruleWidth;
+ line.maxFontSize = 1.0;
+
+ var phi = ruleWidth;
+ if (options.style.id < Style.TEXT.id) {
+ phi = fontMetrics.metrics.xHeight;
+ }
+
+ // Calculate the clearance between the body and line
+ var lineClearance = ruleWidth + phi / 4;
+
+ var innerHeight =
+ (inner.height + inner.depth) * options.style.sizeMultiplier;
+ var minDelimiterHeight = innerHeight + lineClearance + ruleWidth;
+
+ // Create a \surd delimiter of the required minimum size
+ var delim = makeSpan(["sqrt-sign"], [
+ delimiter.customSizedDelim("\\surd", minDelimiterHeight,
+ false, options, group.mode)],
+ options.getColor());
+
+ var delimDepth = (delim.height + delim.depth) - ruleWidth;
+
+ // Adjust the clearance based on the delimiter size
+ if (delimDepth > inner.height + inner.depth + lineClearance) {
+ lineClearance =
+ (lineClearance + delimDepth - inner.height - inner.depth) / 2;
+ }
+
+ // Shift the delimiter so that its top lines up with the top of the line
+ var delimShift = -(inner.height + lineClearance + ruleWidth) + delim.height;
+ delim.style.top = delimShift + "em";
+ delim.height -= delimShift;
+ delim.depth += delimShift;
+
+ // We add a special case here, because even when `inner` is empty, we
+ // still get a line. So, we use a simple heuristic to decide if we
+ // should omit the body entirely. (note this doesn't work for something
+ // like `\sqrt{\rlap{x}}`, but if someone is doing that they deserve for
+ // it not to work.
+ var body;
+ if (inner.height === 0 && inner.depth === 0) {
+ body = makeSpan();
+ } else {
+ body = buildCommon.makeVList([
+ {type: "elem", elem: inner},
+ {type: "kern", size: lineClearance},
+ {type: "elem", elem: line},
+ {type: "kern", size: ruleWidth},
+ ], "firstBaseline", null, options);
+ }
+
+ if (!group.value.index) {
+ return makeSpan(["sqrt", "mord"], [delim, body]);
+ } else {
+ // Handle the optional root index
+
+ // The index is always in scriptscript style
+ var root = buildGroup(
+ group.value.index,
+ options.withStyle(Style.SCRIPTSCRIPT));
+ var rootWrap = makeSpan(
+ [options.style.reset(), Style.SCRIPTSCRIPT.cls()],
+ [root]);
+
+ // Figure out the height and depth of the inner part
+ var innerRootHeight = Math.max(delim.height, body.height);
+ var innerRootDepth = Math.max(delim.depth, body.depth);
+
+ // The amount the index is shifted by. This is taken from the TeX
+ // source, in the definition of `\r@@t`.
+ var toShift = 0.6 * (innerRootHeight - innerRootDepth);
+
+ // Build a VList with the superscript shifted up correctly
+ var rootVList = buildCommon.makeVList(
+ [{type: "elem", elem: rootWrap}],
+ "shift", -toShift, options);
+ // Add a class surrounding it so we can add on the appropriate
+ // kerning
+ var rootVListWrap = makeSpan(["root"], [rootVList]);
+
+ return makeSpan(["sqrt", "mord"], [rootVListWrap, delim, body]);
+ }
+};
+
+groupTypes.sizing = function(group, options, prev) {
+ // Handle sizing operators like \Huge. Real TeX doesn't actually allow
+ // these functions inside of math expressions, so we do some special
+ // handling.
+ var inner = buildExpression(group.value.value,
+ options.withSize(group.value.size), prev);
+
+ var span = makeSpan(["mord"],
+ [makeSpan(["sizing", "reset-" + options.size, group.value.size,
+ options.style.cls()],
+ inner)]);
+
+ // Calculate the correct maxFontSize manually
+ var fontSize = buildCommon.sizingMultiplier[group.value.size];
+ span.maxFontSize = fontSize * options.style.sizeMultiplier;
+
+ return span;
+};
+
+groupTypes.styling = function(group, options, prev) {
+ // Style changes are handled in the TeXbook on pg. 442, Rule 3.
+
+ // Figure out what style we're changing to.
+ var style = {
+ "display": Style.DISPLAY,
+ "text": Style.TEXT,
+ "script": Style.SCRIPT,
+ "scriptscript": Style.SCRIPTSCRIPT,
+ };
+
+ var newStyle = style[group.value.style];
+
+ // Build the inner expression in the new style.
+ var inner = buildExpression(
+ group.value.value, options.withStyle(newStyle), prev);
+
+ return makeSpan([options.style.reset(), newStyle.cls()], inner);
+};
+
+groupTypes.font = function(group, options, prev) {
+ var font = group.value.font;
+ return buildGroup(group.value.body, options.withFont(font), prev);
+};
+
+groupTypes.delimsizing = function(group, options, prev) {
+ var delim = group.value.value;
+
+ if (delim === ".") {
+ // Empty delimiters still count as elements, even though they don't
+ // show anything.
+ return makeSpan([groupToType[group.value.delimType]]);
+ }
+
+ // Use delimiter.sizedDelim to generate the delimiter.
+ return makeSpan(
+ [groupToType[group.value.delimType]],
+ [delimiter.sizedDelim(
+ delim, group.value.size, options, group.mode)]);
+};
+
+groupTypes.leftright = function(group, options, prev) {
+ // Build the inner expression
+ var inner = buildExpression(group.value.body, options.reset());
+
+ var innerHeight = 0;
+ var innerDepth = 0;
+
+ // Calculate its height and depth
+ for (var i = 0; i < inner.length; i++) {
+ innerHeight = Math.max(inner[i].height, innerHeight);
+ innerDepth = Math.max(inner[i].depth, innerDepth);
+ }
+
+ // The size of delimiters is the same, regardless of what style we are
+ // in. Thus, to correctly calculate the size of delimiter we need around
+ // a group, we scale down the inner size based on the size.
+ innerHeight *= options.style.sizeMultiplier;
+ innerDepth *= options.style.sizeMultiplier;
+
+ var leftDelim;
+ if (group.value.left === ".") {
+ // Empty delimiters in \left and \right make null delimiter spaces.
+ leftDelim = makeNullDelimiter(options);
+ } else {
+ // Otherwise, use leftRightDelim to generate the correct sized
+ // delimiter.
+ leftDelim = delimiter.leftRightDelim(
+ group.value.left, innerHeight, innerDepth, options,
+ group.mode);
+ }
+ // Add it to the beginning of the expression
+ inner.unshift(leftDelim);
+
+ var rightDelim;
+ // Same for the right delimiter
+ if (group.value.right === ".") {
+ rightDelim = makeNullDelimiter(options);
+ } else {
+ rightDelim = delimiter.leftRightDelim(
+ group.value.right, innerHeight, innerDepth, options,
+ group.mode);
+ }
+ // Add it to the end of the expression.
+ inner.push(rightDelim);
+
+ return makeSpan(
+ ["minner", options.style.cls()], inner, options.getColor());
+};
+
+groupTypes.rule = function(group, options, prev) {
+ // Make an empty span for the rule
+ var rule = makeSpan(["mord", "rule"], [], options.getColor());
+
+ // Calculate the shift, width, and height of the rule, and account for units
+ var shift = 0;
+ if (group.value.shift) {
+ shift = group.value.shift.number;
+ if (group.value.shift.unit === "ex") {
+ shift *= fontMetrics.metrics.xHeight;
+ }
+ }
+
+ var width = group.value.width.number;
+ if (group.value.width.unit === "ex") {
+ width *= fontMetrics.metrics.xHeight;
+ }
+
+ var height = group.value.height.number;
+ if (group.value.height.unit === "ex") {
+ height *= fontMetrics.metrics.xHeight;
+ }
+
+ // The sizes of rules are absolute, so make it larger if we are in a
+ // smaller style.
+ shift /= options.style.sizeMultiplier;
+ width /= options.style.sizeMultiplier;
+ height /= options.style.sizeMultiplier;
+
+ // Style the rule to the right size
+ rule.style.borderRightWidth = width + "em";
+ rule.style.borderTopWidth = height + "em";
+ rule.style.bottom = shift + "em";
+
+ // Record the height and width
+ rule.width = width;
+ rule.height = height + shift;
+ rule.depth = -shift;
+
+ return rule;
+};
+
+groupTypes.kern = function(group, options, prev) {
+ // Make an empty span for the rule
+ var rule = makeSpan(["mord", "rule"], [], options.getColor());
+
+ var dimension = 0;
+ if (group.value.dimension) {
+ dimension = group.value.dimension.number;
+ if (group.value.dimension.unit === "ex") {
+ dimension *= fontMetrics.metrics.xHeight;
+ }
+ }
+
+ dimension /= options.style.sizeMultiplier;
+
+ rule.style.marginLeft = dimension + "em";
+
+ return rule;
+};
+
+groupTypes.accent = function(group, options, prev) {
+ // Accents are handled in the TeXbook pg. 443, rule 12.
+ var base = group.value.base;
+
+ var supsubGroup;
+ if (group.type === "supsub") {
+ // If our base is a character box, and we have superscripts and
+ // subscripts, the supsub will defer to us. In particular, we want
+ // to attach the superscripts and subscripts to the inner body (so
+ // that the position of the superscripts and subscripts won't be
+ // affected by the height of the accent). We accomplish this by
+ // sticking the base of the accent into the base of the supsub, and
+ // rendering that, while keeping track of where the accent is.
+
+ // The supsub group is the group that was passed in
+ var supsub = group;
+ // The real accent group is the base of the supsub group
+ group = supsub.value.base;
+ // The character box is the base of the accent group
+ base = group.value.base;
+ // Stick the character box into the base of the supsub group
+ supsub.value.base = base;
+
+ // Rerender the supsub group with its new base, and store that
+ // result.
+ supsubGroup = buildGroup(
+ supsub, options.reset(), prev);
+ }
+
+ // Build the base group
+ var body = buildGroup(
+ base, options.withStyle(options.style.cramp()));
+
+ // Calculate the skew of the accent. This is based on the line "If the
+ // nucleus is not a single character, let s = 0; otherwise set s to the
+ // kern amount for the nucleus followed by the \skewchar of its font."
+ // Note that our skew metrics are just the kern between each character
+ // and the skewchar.
+ var skew;
+ if (isCharacterBox(base)) {
+ // If the base is a character box, then we want the skew of the
+ // innermost character. To do that, we find the innermost character:
+ var baseChar = getBaseElem(base);
+ // Then, we render its group to get the symbol inside it
+ var baseGroup = buildGroup(
+ baseChar, options.withStyle(options.style.cramp()));
+ // Finally, we pull the skew off of the symbol.
+ skew = baseGroup.skew;
+ // Note that we now throw away baseGroup, because the layers we
+ // removed with getBaseElem might contain things like \color which
+ // we can't get rid of.
+ // TODO(emily): Find a better way to get the skew
+ } else {
+ skew = 0;
+ }
+
+ // calculate the amount of space between the body and the accent
+ var clearance = Math.min(body.height, fontMetrics.metrics.xHeight);
+
+ // Build the accent
+ var accent = buildCommon.makeSymbol(
+ group.value.accent, "Main-Regular", "math", options.getColor());
+ // Remove the italic correction of the accent, because it only serves to
+ // shift the accent over to a place we don't want.
+ accent.italic = 0;
+
+ // The \vec character that the fonts use is a combining character, and
+ // thus shows up much too far to the left. To account for this, we add a
+ // specific class which shifts the accent over to where we want it.
+ // TODO(emily): Fix this in a better way, like by changing the font
+ var vecClass = group.value.accent === "\\vec" ? "accent-vec" : null;
+
+ var accentBody = makeSpan(["accent-body", vecClass], [
+ makeSpan([], [accent])]);
+
+ accentBody = buildCommon.makeVList([
+ {type: "elem", elem: body},
+ {type: "kern", size: -clearance},
+ {type: "elem", elem: accentBody},
+ ], "firstBaseline", null, options);
+
+ // Shift the accent over by the skew. Note we shift by twice the skew
+ // because we are centering the accent, so by adding 2*skew to the left,
+ // we shift it to the right by 1*skew.
+ accentBody.children[1].style.marginLeft = 2 * skew + "em";
+
+ var accentWrap = makeSpan(["mord", "accent"], [accentBody]);
+
+ if (supsubGroup) {
+ // Here, we replace the "base" child of the supsub with our newly
+ // generated accent.
+ supsubGroup.children[0] = accentWrap;
+
+ // Since we don't rerun the height calculation after replacing the
+ // accent, we manually recalculate height.
+ supsubGroup.height = Math.max(accentWrap.height, supsubGroup.height);
+
+ // Accents should always be ords, even when their innards are not.
+ supsubGroup.classes[0] = "mord";
+
+ return supsubGroup;
+ } else {
+ return accentWrap;
+ }
+};
+
+groupTypes.phantom = function(group, options, prev) {
+ var elements = buildExpression(
+ group.value.value,
+ options.withPhantom(),
+ prev
+ );
+
+ // \phantom isn't supposed to affect the elements it contains.
+ // See "color" for more details.
+ return new buildCommon.makeFragment(elements);
+};
+
+/**
+ * buildGroup is the function that takes a group and calls the correct groupType
+ * function for it. It also handles the interaction of size and style changes
+ * between parents and children.
+ */
+var buildGroup = function(group, options, prev) {
+ if (!group) {
+ return makeSpan();
+ }
+
+ if (groupTypes[group.type]) {
+ // Call the groupTypes function
+ var groupNode = groupTypes[group.type](group, options, prev);
+ var multiplier;
+
+ // If the style changed between the parent and the current group,
+ // account for the size difference
+ if (options.style !== options.parentStyle) {
+ multiplier = options.style.sizeMultiplier /
+ options.parentStyle.sizeMultiplier;
+
+ groupNode.height *= multiplier;
+ groupNode.depth *= multiplier;
+ }
+
+ // If the size changed between the parent and the current group, account
+ // for that size difference.
+ if (options.size !== options.parentSize) {
+ multiplier = buildCommon.sizingMultiplier[options.size] /
+ buildCommon.sizingMultiplier[options.parentSize];
+
+ groupNode.height *= multiplier;
+ groupNode.depth *= multiplier;
+ }
+
+ return groupNode;
+ } else {
+ throw new ParseError(
+ "Got group of unknown type: '" + group.type + "'");
+ }
+};
+
+/**
+ * Take an entire parse tree, and build it into an appropriate set of HTML
+ * nodes.
+ */
+var buildHTML = function(tree, options) {
+ // buildExpression is destructive, so we need to make a clone
+ // of the incoming tree so that it isn't accidentally changed
+ tree = JSON.parse(JSON.stringify(tree));
+
+ // Build the expression contained in the tree
+ var expression = buildExpression(tree, options);
+ var body = makeSpan(["base", options.style.cls()], expression);
+
+ // Add struts, which ensure that the top of the HTML element falls at the
+ // height of the expression, and the bottom of the HTML element falls at the
+ // depth of the expression.
+ var topStrut = makeSpan(["strut"]);
+ var bottomStrut = makeSpan(["strut", "bottom"]);
+
+ topStrut.style.height = body.height + "em";
+ bottomStrut.style.height = (body.height + body.depth) + "em";
+ // We'd like to use `vertical-align: top` but in IE 9 this lowers the
+ // baseline of the box to the bottom of this strut (instead staying in the
+ // normal place) so we use an absolute value for vertical-align instead
+ bottomStrut.style.verticalAlign = -body.depth + "em";
+
+ // Wrap the struts and body together
+ var htmlNode = makeSpan(["katex-html"], [topStrut, bottomStrut, body]);
+
+ htmlNode.setAttribute("aria-hidden", "true");
+
+ return htmlNode;
+};
+
+module.exports = buildHTML;
+
+},{"./ParseError":6,"./Style":9,"./buildCommon":10,"./delimiter":14,"./domTree":15,"./fontMetrics":17,"./utils":25}],12:[function(require,module,exports){
+/**
+ * This file converts a parse tree into a cooresponding MathML tree. The main
+ * entry point is the `buildMathML` function, which takes a parse tree from the
+ * parser.
+ */
+
+var buildCommon = require("./buildCommon");
+var fontMetrics = require("./fontMetrics");
+var mathMLTree = require("./mathMLTree");
+var ParseError = require("./ParseError");
+var symbols = require("./symbols");
+var utils = require("./utils");
+
+var makeSpan = buildCommon.makeSpan;
+var fontMap = buildCommon.fontMap;
+
+/**
+ * Takes a symbol and converts it into a MathML text node after performing
+ * optional replacement from symbols.js.
+ */
+var makeText = function(text, mode) {
+ if (symbols[mode][text] && symbols[mode][text].replace) {
+ text = symbols[mode][text].replace;
+ }
+
+ return new mathMLTree.TextNode(text);
+};
+
+/**
+ * Returns the math variant as a string or null if none is required.
+ */
+var getVariant = function(group, options) {
+ var font = options.font;
+ if (!font) {
+ return null;
+ }
+
+ var mode = group.mode;
+ if (font === "mathit") {
+ return "italic";
+ }
+
+ var value = group.value;
+ if (utils.contains(["\\imath", "\\jmath"], value)) {
+ return null;
+ }
+
+ if (symbols[mode][value] && symbols[mode][value].replace) {
+ value = symbols[mode][value].replace;
+ }
+
+ var fontName = fontMap[font].fontName;
+ if (fontMetrics.getCharacterMetrics(value, fontName)) {
+ return fontMap[options.font].variant;
+ }
+
+ return null;
+};
+
+/**
+ * Functions for handling the different types of groups found in the parse
+ * tree. Each function should take a parse group and return a MathML node.
+ */
+var groupTypes = {};
+
+groupTypes.mathord = function(group, options) {
+ var node = new mathMLTree.MathNode(
+ "mi",
+ [makeText(group.value, group.mode)]);
+
+ var variant = getVariant(group, options);
+ if (variant) {
+ node.setAttribute("mathvariant", variant);
+ }
+ return node;
+};
+
+groupTypes.textord = function(group, options) {
+ var text = makeText(group.value, group.mode);
+
+ var variant = getVariant(group, options) || "normal";
+
+ var node;
+ if (/[0-9]/.test(group.value)) {
+ // TODO(kevinb) merge adjacent <mn> nodes
+ // do it as a post processing step
+ node = new mathMLTree.MathNode("mn", [text]);
+ if (options.font) {
+ node.setAttribute("mathvariant", variant);
+ }
+ } else {
+ node = new mathMLTree.MathNode("mi", [text]);
+ node.setAttribute("mathvariant", variant);
+ }
+
+ return node;
+};
+
+groupTypes.bin = function(group) {
+ var node = new mathMLTree.MathNode(
+ "mo", [makeText(group.value, group.mode)]);
+
+ return node;
+};
+
+groupTypes.rel = function(group) {
+ var node = new mathMLTree.MathNode(
+ "mo", [makeText(group.value, group.mode)]);
+
+ return node;
+};
+
+groupTypes.open = function(group) {
+ var node = new mathMLTree.MathNode(
+ "mo", [makeText(group.value, group.mode)]);
+
+ return node;
+};
+
+groupTypes.close = function(group) {
+ var node = new mathMLTree.MathNode(
+ "mo", [makeText(group.value, group.mode)]);
+
+ return node;
+};
+
+groupTypes.inner = function(group) {
+ var node = new mathMLTree.MathNode(
+ "mo", [makeText(group.value, group.mode)]);
+
+ return node;
+};
+
+groupTypes.punct = function(group) {
+ var node = new mathMLTree.MathNode(
+ "mo", [makeText(group.value, group.mode)]);
+
+ node.setAttribute("separator", "true");
+
+ return node;
+};
+
+groupTypes.ordgroup = function(group, options) {
+ var inner = buildExpression(group.value, options);
+
+ var node = new mathMLTree.MathNode("mrow", inner);
+
+ return node;
+};
+
+groupTypes.text = function(group, options) {
+ var inner = buildExpression(group.value.body, options);
+
+ var node = new mathMLTree.MathNode("mtext", inner);
+
+ return node;
+};
+
+groupTypes.color = function(group, options) {
+ var inner = buildExpression(group.value.value, options);
+
+ var node = new mathMLTree.MathNode("mstyle", inner);
+
+ node.setAttribute("mathcolor", group.value.color);
+
+ return node;
+};
+
+groupTypes.supsub = function(group, options) {
+ var children = [buildGroup(group.value.base, options)];
+
+ if (group.value.sub) {
+ children.push(buildGroup(group.value.sub, options));
+ }
+
+ if (group.value.sup) {
+ children.push(buildGroup(group.value.sup, options));
+ }
+
+ var nodeType;
+ if (!group.value.sub) {
+ nodeType = "msup";
+ } else if (!group.value.sup) {
+ nodeType = "msub";
+ } else {
+ nodeType = "msubsup";
+ }
+
+ var node = new mathMLTree.MathNode(nodeType, children);
+
+ return node;
+};
+
+groupTypes.genfrac = function(group, options) {
+ var node = new mathMLTree.MathNode(
+ "mfrac",
+ [buildGroup(group.value.numer, options),
+ buildGroup(group.value.denom, options)]);
+
+ if (!group.value.hasBarLine) {
+ node.setAttribute("linethickness", "0px");
+ }
+
+ if (group.value.leftDelim != null || group.value.rightDelim != null) {
+ var withDelims = [];
+
+ if (group.value.leftDelim != null) {
+ var leftOp = new mathMLTree.MathNode(
+ "mo", [new mathMLTree.TextNode(group.value.leftDelim)]);
+
+ leftOp.setAttribute("fence", "true");
+
+ withDelims.push(leftOp);
+ }
+
+ withDelims.push(node);
+
+ if (group.value.rightDelim != null) {
+ var rightOp = new mathMLTree.MathNode(
+ "mo", [new mathMLTree.TextNode(group.value.rightDelim)]);
+
+ rightOp.setAttribute("fence", "true");
+
+ withDelims.push(rightOp);
+ }
+
+ var outerNode = new mathMLTree.MathNode("mrow", withDelims);
+
+ return outerNode;
+ }
+
+ return node;
+};
+
+groupTypes.array = function(group, options) {
+ return new mathMLTree.MathNode(
+ "mtable", group.value.body.map(function(row) {
+ return new mathMLTree.MathNode(
+ "mtr", row.map(function(cell) {
+ return new mathMLTree.MathNode(
+ "mtd", [buildGroup(cell, options)]);
+ }));
+ }));
+};
+
+groupTypes.sqrt = function(group, options) {
+ var node;
+ if (group.value.index) {
+ node = new mathMLTree.MathNode(
+ "mroot", [
+ buildGroup(group.value.body, options),
+ buildGroup(group.value.index, options),
+ ]);
+ } else {
+ node = new mathMLTree.MathNode(
+ "msqrt", [buildGroup(group.value.body, options)]);
+ }
+
+ return node;
+};
+
+groupTypes.leftright = function(group, options) {
+ var inner = buildExpression(group.value.body, options);
+
+ if (group.value.left !== ".") {
+ var leftNode = new mathMLTree.MathNode(
+ "mo", [makeText(group.value.left, group.mode)]);
+
+ leftNode.setAttribute("fence", "true");
+
+ inner.unshift(leftNode);
+ }
+
+ if (group.value.right !== ".") {
+ var rightNode = new mathMLTree.MathNode(
+ "mo", [makeText(group.value.right, group.mode)]);
+
+ rightNode.setAttribute("fence", "true");
+
+ inner.push(rightNode);
+ }
+
+ var outerNode = new mathMLTree.MathNode("mrow", inner);
+
+ return outerNode;
+};
+
+groupTypes.accent = function(group, options) {
+ var accentNode = new mathMLTree.MathNode(
+ "mo", [makeText(group.value.accent, group.mode)]);
+
+ var node = new mathMLTree.MathNode(
+ "mover",
+ [buildGroup(group.value.base, options),
+ accentNode]);
+
+ node.setAttribute("accent", "true");
+
+ return node;
+};
+
+groupTypes.spacing = function(group) {
+ var node;
+
+ if (group.value === "\\ " || group.value === "\\space" ||
+ group.value === " " || group.value === "~") {
+ node = new mathMLTree.MathNode(
+ "mtext", [new mathMLTree.TextNode("\u00a0")]);
+ } else {
+ node = new mathMLTree.MathNode("mspace");
+
+ node.setAttribute(
+ "width", buildCommon.spacingFunctions[group.value].size);
+ }
+
+ return node;
+};
+
+groupTypes.op = function(group) {
+ var node;
+
+ // TODO(emily): handle big operators using the `largeop` attribute
+
+ if (group.value.symbol) {
+ // This is a symbol. Just add the symbol.
+ node = new mathMLTree.MathNode(
+ "mo", [makeText(group.value.body, group.mode)]);
+ } else {
+ // This is a text operator. Add all of the characters from the
+ // operator's name.
+ // TODO(emily): Add a space in the middle of some of these
+ // operators, like \limsup.
+ node = new mathMLTree.MathNode(
+ "mi", [new mathMLTree.TextNode(group.value.body.slice(1))]);
+ }
+
+ return node;
+};
+
+groupTypes.katex = function(group) {
+ var node = new mathMLTree.MathNode(
+ "mtext", [new mathMLTree.TextNode("KaTeX")]);
+
+ return node;
+};
+
+groupTypes.font = function(group, options) {
+ var font = group.value.font;
+ return buildGroup(group.value.body, options.withFont(font));
+};
+
+groupTypes.delimsizing = function(group) {
+ var children = [];
+
+ if (group.value.value !== ".") {
+ children.push(makeText(group.value.value, group.mode));
+ }
+
+ var node = new mathMLTree.MathNode("mo", children);
+
+ if (group.value.delimType === "open" ||
+ group.value.delimType === "close") {
+ // Only some of the delimsizing functions act as fences, and they
+ // return "open" or "close" delimTypes.
+ node.setAttribute("fence", "true");
+ } else {
+ // Explicitly disable fencing if it's not a fence, to override the
+ // defaults.
+ node.setAttribute("fence", "false");
+ }
+
+ return node;
+};
+
+groupTypes.styling = function(group, options) {
+ var inner = buildExpression(group.value.value, options);
+
+ var node = new mathMLTree.MathNode("mstyle", inner);
+
+ var styleAttributes = {
+ "display": ["0", "true"],
+ "text": ["0", "false"],
+ "script": ["1", "false"],
+ "scriptscript": ["2", "false"],
+ };
+
+ var attr = styleAttributes[group.value.style];
+
+ node.setAttribute("scriptlevel", attr[0]);
+ node.setAttribute("displaystyle", attr[1]);
+
+ return node;
+};
+
+groupTypes.sizing = function(group, options) {
+ var inner = buildExpression(group.value.value, options);
+
+ var node = new mathMLTree.MathNode("mstyle", inner);
+
+ // TODO(emily): This doesn't produce the correct size for nested size
+ // changes, because we don't keep state of what style we're currently
+ // in, so we can't reset the size to normal before changing it. Now
+ // that we're passing an options parameter we should be able to fix
+ // this.
+ node.setAttribute(
+ "mathsize", buildCommon.sizingMultiplier[group.value.size] + "em");
+
+ return node;
+};
+
+groupTypes.overline = function(group, options) {
+ var operator = new mathMLTree.MathNode(
+ "mo", [new mathMLTree.TextNode("\u203e")]);
+ operator.setAttribute("stretchy", "true");
+
+ var node = new mathMLTree.MathNode(
+ "mover",
+ [buildGroup(group.value.body, options),
+ operator]);
+ node.setAttribute("accent", "true");
+
+ return node;
+};
+
+groupTypes.underline = function(group, options) {
+ var operator = new mathMLTree.MathNode(
+ "mo", [new mathMLTree.TextNode("\u203e")]);
+ operator.setAttribute("stretchy", "true");
+
+ var node = new mathMLTree.MathNode(
+ "munder",
+ [buildGroup(group.value.body, options),
+ operator]);
+ node.setAttribute("accentunder", "true");
+
+ return node;
+};
+
+groupTypes.rule = function(group) {
+ // TODO(emily): Figure out if there's an actual way to draw black boxes
+ // in MathML.
+ var node = new mathMLTree.MathNode("mrow");
+
+ return node;
+};
+
+groupTypes.kern = function(group) {
+ // TODO(kevin): Figure out if there's a way to add space in MathML
+ var node = new mathMLTree.MathNode("mrow");
+
+ return node;
+};
+
+groupTypes.llap = function(group, options) {
+ var node = new mathMLTree.MathNode(
+ "mpadded", [buildGroup(group.value.body, options)]);
+
+ node.setAttribute("lspace", "-1width");
+ node.setAttribute("width", "0px");
+
+ return node;
+};
+
+groupTypes.rlap = function(group, options) {
+ var node = new mathMLTree.MathNode(
+ "mpadded", [buildGroup(group.value.body, options)]);
+
+ node.setAttribute("width", "0px");
+
+ return node;
+};
+
+groupTypes.phantom = function(group, options, prev) {
+ var inner = buildExpression(group.value.value, options);
+ return new mathMLTree.MathNode("mphantom", inner);
+};
+
+/**
+ * Takes a list of nodes, builds them, and returns a list of the generated
+ * MathML nodes. A little simpler than the HTML version because we don't do any
+ * previous-node handling.
+ */
+var buildExpression = function(expression, options) {
+ var groups = [];
+ for (var i = 0; i < expression.length; i++) {
+ var group = expression[i];
+ groups.push(buildGroup(group, options));
+ }
+ return groups;
+};
+
+/**
+ * Takes a group from the parser and calls the appropriate groupTypes function
+ * on it to produce a MathML node.
+ */
+var buildGroup = function(group, options) {
+ if (!group) {
+ return new mathMLTree.MathNode("mrow");
+ }
+
+ if (groupTypes[group.type]) {
+ // Call the groupTypes function
+ return groupTypes[group.type](group, options);
+ } else {
+ throw new ParseError(
+ "Got group of unknown type: '" + group.type + "'");
+ }
+};
+
+/**
+ * Takes a full parse tree and settings and builds a MathML representation of
+ * it. In particular, we put the elements from building the parse tree into a
+ * <semantics> tag so we can also include that TeX source as an annotation.
+ *
+ * Note that we actually return a domTree element with a `<math>` inside it so
+ * we can do appropriate styling.
+ */
+var buildMathML = function(tree, texExpression, options) {
+ var expression = buildExpression(tree, options);
+
+ // Wrap up the expression in an mrow so it is presented in the semantics
+ // tag correctly.
+ var wrapper = new mathMLTree.MathNode("mrow", expression);
+
+ // Build a TeX annotation of the source
+ var annotation = new mathMLTree.MathNode(
+ "annotation", [new mathMLTree.TextNode(texExpression)]);
+
+ annotation.setAttribute("encoding", "application/x-tex");
+
+ var semantics = new mathMLTree.MathNode(
+ "semantics", [wrapper, annotation]);
+
+ var math = new mathMLTree.MathNode("math", [semantics]);
+
+ // You can't style <math> nodes, so we wrap the node in a span.
+ return makeSpan(["katex-mathml"], [math]);
+};
+
+module.exports = buildMathML;
+
+},{"./ParseError":6,"./buildCommon":10,"./fontMetrics":17,"./mathMLTree":20,"./symbols":23,"./utils":25}],13:[function(require,module,exports){
+var buildHTML = require("./buildHTML");
+var buildMathML = require("./buildMathML");
+var buildCommon = require("./buildCommon");
+var Options = require("./Options");
+var Settings = require("./Settings");
+var Style = require("./Style");
+
+var makeSpan = buildCommon.makeSpan;
+
+var buildTree = function(tree, expression, settings) {
+ settings = settings || new Settings({});
+
+ var startStyle = Style.TEXT;
+ if (settings.displayMode) {
+ startStyle = Style.DISPLAY;
+ }
+
+ // Setup the default options
+ var options = new Options({
+ style: startStyle,
+ size: "size5",
+ });
+
+ // `buildHTML` sometimes messes with the parse tree (like turning bins ->
+ // ords), so we build the MathML version first.
+ var mathMLNode = buildMathML(tree, expression, options);
+ var htmlNode = buildHTML(tree, options);
+
+ var katexNode = makeSpan(["katex"], [
+ mathMLNode, htmlNode,
+ ]);
+
+ if (settings.displayMode) {
+ return makeSpan(["katex-display"], [katexNode]);
+ } else {
+ return katexNode;
+ }
+};
+
+module.exports = buildTree;
+
+},{"./Options":5,"./Settings":8,"./Style":9,"./buildCommon":10,"./buildHTML":11,"./buildMathML":12}],14:[function(require,module,exports){
+/**
+ * This file deals with creating delimiters of various sizes. The TeXbook
+ * discusses these routines on page 441-442, in the "Another subroutine sets box
+ * x to a specified variable delimiter" paragraph.
+ *
+ * There are three main routines here. `makeSmallDelim` makes a delimiter in the
+ * normal font, but in either text, script, or scriptscript style.
+ * `makeLargeDelim` makes a delimiter in textstyle, but in one of the Size1,
+ * Size2, Size3, or Size4 fonts. `makeStackedDelim` makes a delimiter out of
+ * smaller pieces that are stacked on top of one another.
+ *
+ * The functions take a parameter `center`, which determines if the delimiter
+ * should be centered around the axis.
+ *
+ * Then, there are three exposed functions. `sizedDelim` makes a delimiter in
+ * one of the given sizes. This is used for things like `\bigl`.
+ * `customSizedDelim` makes a delimiter with a given total height+depth. It is
+ * called in places like `\sqrt`. `leftRightDelim` makes an appropriate
+ * delimiter which surrounds an expression of a given height an depth. It is
+ * used in `\left` and `\right`.
+ */
+
+var ParseError = require("./ParseError");
+var Style = require("./Style");
+
+var buildCommon = require("./buildCommon");
+var fontMetrics = require("./fontMetrics");
+var symbols = require("./symbols");
+var utils = require("./utils");
+
+var makeSpan = buildCommon.makeSpan;
+
+/**
+ * Get the metrics for a given symbol and font, after transformation (i.e.
+ * after following replacement from symbols.js)
+ */
+var getMetrics = function(symbol, font) {
+ if (symbols.math[symbol] && symbols.math[symbol].replace) {
+ return fontMetrics.getCharacterMetrics(
+ symbols.math[symbol].replace, font);
+ } else {
+ return fontMetrics.getCharacterMetrics(
+ symbol, font);
+ }
+};
+
+/**
+ * Builds a symbol in the given font size (note size is an integer)
+ */
+var mathrmSize = function(value, size, mode) {
+ return buildCommon.makeSymbol(value, "Size" + size + "-Regular", mode);
+};
+
+/**
+ * Puts a delimiter span in a given style, and adds appropriate height, depth,
+ * and maxFontSizes.
+ */
+var styleWrap = function(delim, toStyle, options) {
+ var span = makeSpan(
+ ["style-wrap", options.style.reset(), toStyle.cls()], [delim]);
+
+ var multiplier = toStyle.sizeMultiplier / options.style.sizeMultiplier;
+
+ span.height *= multiplier;
+ span.depth *= multiplier;
+ span.maxFontSize = toStyle.sizeMultiplier;
+
+ return span;
+};
+
+/**
+ * Makes a small delimiter. This is a delimiter that comes in the Main-Regular
+ * font, but is restyled to either be in textstyle, scriptstyle, or
+ * scriptscriptstyle.
+ */
+var makeSmallDelim = function(delim, style, center, options, mode) {
+ var text = buildCommon.makeSymbol(delim, "Main-Regular", mode);
+
+ var span = styleWrap(text, style, options);
+
+ if (center) {
+ var shift =
+ (1 - options.style.sizeMultiplier / style.sizeMultiplier) *
+ fontMetrics.metrics.axisHeight;
+
+ span.style.top = shift + "em";
+ span.height -= shift;
+ span.depth += shift;
+ }
+
+ return span;
+};
+
+/**
+ * Makes a large delimiter. This is a delimiter that comes in the Size1, Size2,
+ * Size3, or Size4 fonts. It is always rendered in textstyle.
+ */
+var makeLargeDelim = function(delim, size, center, options, mode) {
+ var inner = mathrmSize(delim, size, mode);
+
+ var span = styleWrap(
+ makeSpan(["delimsizing", "size" + size],
+ [inner], options.getColor()),
+ Style.TEXT, options);
+
+ if (center) {
+ var shift = (1 - options.style.sizeMultiplier) *
+ fontMetrics.metrics.axisHeight;
+
+ span.style.top = shift + "em";
+ span.height -= shift;
+ span.depth += shift;
+ }
+
+ return span;
+};
+
+/**
+ * Make an inner span with the given offset and in the given font. This is used
+ * in `makeStackedDelim` to make the stacking pieces for the delimiter.
+ */
+var makeInner = function(symbol, font, mode) {
+ var sizeClass;
+ // Apply the correct CSS class to choose the right font.
+ if (font === "Size1-Regular") {
+ sizeClass = "delim-size1";
+ } else if (font === "Size4-Regular") {
+ sizeClass = "delim-size4";
+ }
+
+ var inner = makeSpan(
+ ["delimsizinginner", sizeClass],
+ [makeSpan([], [buildCommon.makeSymbol(symbol, font, mode)])]);
+
+ // Since this will be passed into `makeVList` in the end, wrap the element
+ // in the appropriate tag that VList uses.
+ return {type: "elem", elem: inner};
+};
+
+/**
+ * Make a stacked delimiter out of a given delimiter, with the total height at
+ * least `heightTotal`. This routine is mentioned on page 442 of the TeXbook.
+ */
+var makeStackedDelim = function(delim, heightTotal, center, options, mode) {
+ // There are four parts, the top, an optional middle, a repeated part, and a
+ // bottom.
+ var top;
+ var middle;
+ var repeat;
+ var bottom;
+ top = repeat = bottom = delim;
+ middle = null;
+ // Also keep track of what font the delimiters are in
+ var font = "Size1-Regular";
+
+ // We set the parts and font based on the symbol. Note that we use
+ // '\u23d0' instead of '|' and '\u2016' instead of '\\|' for the
+ // repeats of the arrows
+ if (delim === "\\uparrow") {
+ repeat = bottom = "\u23d0";
+ } else if (delim === "\\Uparrow") {
+ repeat = bottom = "\u2016";
+ } else if (delim === "\\downarrow") {
+ top = repeat = "\u23d0";
+ } else if (delim === "\\Downarrow") {
+ top = repeat = "\u2016";
+ } else if (delim === "\\updownarrow") {
+ top = "\\uparrow";
+ repeat = "\u23d0";
+ bottom = "\\downarrow";
+ } else if (delim === "\\Updownarrow") {
+ top = "\\Uparrow";
+ repeat = "\u2016";
+ bottom = "\\Downarrow";
+ } else if (delim === "[" || delim === "\\lbrack") {
+ top = "\u23a1";
+ repeat = "\u23a2";
+ bottom = "\u23a3";
+ font = "Size4-Regular";
+ } else if (delim === "]" || delim === "\\rbrack") {
+ top = "\u23a4";
+ repeat = "\u23a5";
+ bottom = "\u23a6";
+ font = "Size4-Regular";
+ } else if (delim === "\\lfloor") {
+ repeat = top = "\u23a2";
+ bottom = "\u23a3";
+ font = "Size4-Regular";
+ } else if (delim === "\\lceil") {
+ top = "\u23a1";
+ repeat = bottom = "\u23a2";
+ font = "Size4-Regular";
+ } else if (delim === "\\rfloor") {
+ repeat = top = "\u23a5";
+ bottom = "\u23a6";
+ font = "Size4-Regular";
+ } else if (delim === "\\rceil") {
+ top = "\u23a4";
+ repeat = bottom = "\u23a5";
+ font = "Size4-Regular";
+ } else if (delim === "(") {
+ top = "\u239b";
+ repeat = "\u239c";
+ bottom = "\u239d";
+ font = "Size4-Regular";
+ } else if (delim === ")") {
+ top = "\u239e";
+ repeat = "\u239f";
+ bottom = "\u23a0";
+ font = "Size4-Regular";
+ } else if (delim === "\\{" || delim === "\\lbrace") {
+ top = "\u23a7";
+ middle = "\u23a8";
+ bottom = "\u23a9";
+ repeat = "\u23aa";
+ font = "Size4-Regular";
+ } else if (delim === "\\}" || delim === "\\rbrace") {
+ top = "\u23ab";
+ middle = "\u23ac";
+ bottom = "\u23ad";
+ repeat = "\u23aa";
+ font = "Size4-Regular";
+ } else if (delim === "\\lgroup") {
+ top = "\u23a7";
+ bottom = "\u23a9";
+ repeat = "\u23aa";
+ font = "Size4-Regular";
+ } else if (delim === "\\rgroup") {
+ top = "\u23ab";
+ bottom = "\u23ad";
+ repeat = "\u23aa";
+ font = "Size4-Regular";
+ } else if (delim === "\\lmoustache") {
+ top = "\u23a7";
+ bottom = "\u23ad";
+ repeat = "\u23aa";
+ font = "Size4-Regular";
+ } else if (delim === "\\rmoustache") {
+ top = "\u23ab";
+ bottom = "\u23a9";
+ repeat = "\u23aa";
+ font = "Size4-Regular";
+ } else if (delim === "\\surd") {
+ top = "\ue001";
+ bottom = "\u23b7";
+ repeat = "\ue000";
+ font = "Size4-Regular";
+ }
+
+ // Get the metrics of the four sections
+ var topMetrics = getMetrics(top, font);
+ var topHeightTotal = topMetrics.height + topMetrics.depth;
+ var repeatMetrics = getMetrics(repeat, font);
+ var repeatHeightTotal = repeatMetrics.height + repeatMetrics.depth;
+ var bottomMetrics = getMetrics(bottom, font);
+ var bottomHeightTotal = bottomMetrics.height + bottomMetrics.depth;
+ var middleHeightTotal = 0;
+ var middleFactor = 1;
+ if (middle !== null) {
+ var middleMetrics = getMetrics(middle, font);
+ middleHeightTotal = middleMetrics.height + middleMetrics.depth;
+ middleFactor = 2; // repeat symmetrically above and below middle
+ }
+
+ // Calcuate the minimal height that the delimiter can have.
+ // It is at least the size of the top, bottom, and optional middle combined.
+ var minHeight = topHeightTotal + bottomHeightTotal + middleHeightTotal;
+
+ // Compute the number of copies of the repeat symbol we will need
+ var repeatCount = Math.ceil(
+ (heightTotal - minHeight) / (middleFactor * repeatHeightTotal));
+
+ // Compute the total height of the delimiter including all the symbols
+ var realHeightTotal =
+ minHeight + repeatCount * middleFactor * repeatHeightTotal;
+
+ // The center of the delimiter is placed at the center of the axis. Note
+ // that in this context, "center" means that the delimiter should be
+ // centered around the axis in the current style, while normally it is
+ // centered around the axis in textstyle.
+ var axisHeight = fontMetrics.metrics.axisHeight;
+ if (center) {
+ axisHeight *= options.style.sizeMultiplier;
+ }
+ // Calculate the depth
+ var depth = realHeightTotal / 2 - axisHeight;
+
+ // Now, we start building the pieces that will go into the vlist
+
+ // Keep a list of the inner pieces
+ var inners = [];
+
+ // Add the bottom symbol
+ inners.push(makeInner(bottom, font, mode));
+
+ var i;
+ if (middle === null) {
+ // Add that many symbols
+ for (i = 0; i < repeatCount; i++) {
+ inners.push(makeInner(repeat, font, mode));
+ }
+ } else {
+ // When there is a middle bit, we need the middle part and two repeated
+ // sections
+ for (i = 0; i < repeatCount; i++) {
+ inners.push(makeInner(repeat, font, mode));
+ }
+ inners.push(makeInner(middle, font, mode));
+ for (i = 0; i < repeatCount; i++) {
+ inners.push(makeInner(repeat, font, mode));
+ }
+ }
+
+ // Add the top symbol
+ inners.push(makeInner(top, font, mode));
+
+ // Finally, build the vlist
+ var inner = buildCommon.makeVList(inners, "bottom", depth, options);
+
+ return styleWrap(
+ makeSpan(["delimsizing", "mult"], [inner], options.getColor()),
+ Style.TEXT, options);
+};
+
+// There are three kinds of delimiters, delimiters that stack when they become
+// too large
+var stackLargeDelimiters = [
+ "(", ")", "[", "\\lbrack", "]", "\\rbrack",
+ "\\{", "\\lbrace", "\\}", "\\rbrace",
+ "\\lfloor", "\\rfloor", "\\lceil", "\\rceil",
+ "\\surd",
+];
+
+// delimiters that always stack
+var stackAlwaysDelimiters = [
+ "\\uparrow", "\\downarrow", "\\updownarrow",
+ "\\Uparrow", "\\Downarrow", "\\Updownarrow",
+ "|", "\\|", "\\vert", "\\Vert",
+ "\\lvert", "\\rvert", "\\lVert", "\\rVert",
+ "\\lgroup", "\\rgroup", "\\lmoustache", "\\rmoustache",
+];
+
+// and delimiters that never stack
+var stackNeverDelimiters = [
+ "<", ">", "\\langle", "\\rangle", "/", "\\backslash", "\\lt", "\\gt",
+];
+
+// Metrics of the different sizes. Found by looking at TeX's output of
+// $\bigl| // \Bigl| \biggl| \Biggl| \showlists$
+// Used to create stacked delimiters of appropriate sizes in makeSizedDelim.
+var sizeToMaxHeight = [0, 1.2, 1.8, 2.4, 3.0];
+
+/**
+ * Used to create a delimiter of a specific size, where `size` is 1, 2, 3, or 4.
+ */
+var makeSizedDelim = function(delim, size, options, mode) {
+ // < and > turn into \langle and \rangle in delimiters
+ if (delim === "<" || delim === "\\lt") {
+ delim = "\\langle";
+ } else if (delim === ">" || delim === "\\gt") {
+ delim = "\\rangle";
+ }
+
+ // Sized delimiters are never centered.
+ if (utils.contains(stackLargeDelimiters, delim) ||
+ utils.contains(stackNeverDelimiters, delim)) {
+ return makeLargeDelim(delim, size, false, options, mode);
+ } else if (utils.contains(stackAlwaysDelimiters, delim)) {
+ return makeStackedDelim(
+ delim, sizeToMaxHeight[size], false, options, mode);
+ } else {
+ throw new ParseError("Illegal delimiter: '" + delim + "'");
+ }
+};
+
+/**
+ * There are three different sequences of delimiter sizes that the delimiters
+ * follow depending on the kind of delimiter. This is used when creating custom
+ * sized delimiters to decide whether to create a small, large, or stacked
+ * delimiter.
+ *
+ * In real TeX, these sequences aren't explicitly defined, but are instead
+ * defined inside the font metrics. Since there are only three sequences that
+ * are possible for the delimiters that TeX defines, it is easier to just encode
+ * them explicitly here.
+ */
+
+// Delimiters that never stack try small delimiters and large delimiters only
+var stackNeverDelimiterSequence = [
+ {type: "small", style: Style.SCRIPTSCRIPT},
+ {type: "small", style: Style.SCRIPT},
+ {type: "small", style: Style.TEXT},
+ {type: "large", size: 1},
+ {type: "large", size: 2},
+ {type: "large", size: 3},
+ {type: "large", size: 4},
+];
+
+// Delimiters that always stack try the small delimiters first, then stack
+var stackAlwaysDelimiterSequence = [
+ {type: "small", style: Style.SCRIPTSCRIPT},
+ {type: "small", style: Style.SCRIPT},
+ {type: "small", style: Style.TEXT},
+ {type: "stack"},
+];
+
+// Delimiters that stack when large try the small and then large delimiters, and
+// stack afterwards
+var stackLargeDelimiterSequence = [
+ {type: "small", style: Style.SCRIPTSCRIPT},
+ {type: "small", style: Style.SCRIPT},
+ {type: "small", style: Style.TEXT},
+ {type: "large", size: 1},
+ {type: "large", size: 2},
+ {type: "large", size: 3},
+ {type: "large", size: 4},
+ {type: "stack"},
+];
+
+/**
+ * Get the font used in a delimiter based on what kind of delimiter it is.
+ */
+var delimTypeToFont = function(type) {
+ if (type.type === "small") {
+ return "Main-Regular";
+ } else if (type.type === "large") {
+ return "Size" + type.size + "-Regular";
+ } else if (type.type === "stack") {
+ return "Size4-Regular";
+ }
+};
+
+/**
+ * Traverse a sequence of types of delimiters to decide what kind of delimiter
+ * should be used to create a delimiter of the given height+depth.
+ */
+var traverseSequence = function(delim, height, sequence, options) {
+ // Here, we choose the index we should start at in the sequences. In smaller
+ // sizes (which correspond to larger numbers in style.size) we start earlier
+ // in the sequence. Thus, scriptscript starts at index 3-3=0, script starts
+ // at index 3-2=1, text starts at 3-1=2, and display starts at min(2,3-0)=2
+ var start = Math.min(2, 3 - options.style.size);
+ for (var i = start; i < sequence.length; i++) {
+ if (sequence[i].type === "stack") {
+ // This is always the last delimiter, so we just break the loop now.
+ break;
+ }
+
+ var metrics = getMetrics(delim, delimTypeToFont(sequence[i]));
+ var heightDepth = metrics.height + metrics.depth;
+
+ // Small delimiters are scaled down versions of the same font, so we
+ // account for the style change size.
+
+ if (sequence[i].type === "small") {
+ heightDepth *= sequence[i].style.sizeMultiplier;
+ }
+
+ // Check if the delimiter at this size works for the given height.
+ if (heightDepth > height) {
+ return sequence[i];
+ }
+ }
+
+ // If we reached the end of the sequence, return the last sequence element.
+ return sequence[sequence.length - 1];
+};
+
+/**
+ * Make a delimiter of a given height+depth, with optional centering. Here, we
+ * traverse the sequences, and create a delimiter that the sequence tells us to.
+ */
+var makeCustomSizedDelim = function(delim, height, center, options, mode) {
+ if (delim === "<" || delim === "\\lt") {
+ delim = "\\langle";
+ } else if (delim === ">" || delim === "\\gt") {
+ delim = "\\rangle";
+ }
+
+ // Decide what sequence to use
+ var sequence;
+ if (utils.contains(stackNeverDelimiters, delim)) {
+ sequence = stackNeverDelimiterSequence;
+ } else if (utils.contains(stackLargeDelimiters, delim)) {
+ sequence = stackLargeDelimiterSequence;
+ } else {
+ sequence = stackAlwaysDelimiterSequence;
+ }
+
+ // Look through the sequence
+ var delimType = traverseSequence(delim, height, sequence, options);
+
+ // Depending on the sequence element we decided on, call the appropriate
+ // function.
+ if (delimType.type === "small") {
+ return makeSmallDelim(delim, delimType.style, center, options, mode);
+ } else if (delimType.type === "large") {
+ return makeLargeDelim(delim, delimType.size, center, options, mode);
+ } else if (delimType.type === "stack") {
+ return makeStackedDelim(delim, height, center, options, mode);
+ }
+};
+
+/**
+ * Make a delimiter for use with `\left` and `\right`, given a height and depth
+ * of an expression that the delimiters surround.
+ */
+var makeLeftRightDelim = function(delim, height, depth, options, mode) {
+ // We always center \left/\right delimiters, so the axis is always shifted
+ var axisHeight =
+ fontMetrics.metrics.axisHeight * options.style.sizeMultiplier;
+
+ // Taken from TeX source, tex.web, function make_left_right
+ var delimiterFactor = 901;
+ var delimiterExtend = 5.0 / fontMetrics.metrics.ptPerEm;
+
+ var maxDistFromAxis = Math.max(
+ height - axisHeight, depth + axisHeight);
+
+ var totalHeight = Math.max(
+ // In real TeX, calculations are done using integral values which are
+ // 65536 per pt, or 655360 per em. So, the division here truncates in
+ // TeX but doesn't here, producing different results. If we wanted to
+ // exactly match TeX's calculation, we could do
+ // Math.floor(655360 * maxDistFromAxis / 500) *
+ // delimiterFactor / 655360
+ // (To see the difference, compare
+ // x^{x^{\left(\rule{0.1em}{0.68em}\right)}}
+ // in TeX and KaTeX)
+ maxDistFromAxis / 500 * delimiterFactor,
+ 2 * maxDistFromAxis - delimiterExtend);
+
+ // Finally, we defer to `makeCustomSizedDelim` with our calculated total
+ // height
+ return makeCustomSizedDelim(delim, totalHeight, true, options, mode);
+};
+
+module.exports = {
+ sizedDelim: makeSizedDelim,
+ customSizedDelim: makeCustomSizedDelim,
+ leftRightDelim: makeLeftRightDelim,
+};
+
+},{"./ParseError":6,"./Style":9,"./buildCommon":10,"./fontMetrics":17,"./symbols":23,"./utils":25}],15:[function(require,module,exports){
+/**
+ * These objects store the data about the DOM nodes we create, as well as some
+ * extra data. They can then be transformed into real DOM nodes with the
+ * `toNode` function or HTML markup using `toMarkup`. They are useful for both
+ * storing extra properties on the nodes, as well as providing a way to easily
+ * work with the DOM.
+ *
+ * Similar functions for working with MathML nodes exist in mathMLTree.js.
+ */
+var unicodeRegexes = require("./unicodeRegexes");
+var utils = require("./utils");
+
+/**
+ * Create an HTML className based on a list of classes. In addition to joining
+ * with spaces, we also remove null or empty classes.
+ */
+var createClass = function(classes) {
+ classes = classes.slice();
+ for (var i = classes.length - 1; i >= 0; i--) {
+ if (!classes[i]) {
+ classes.splice(i, 1);
+ }
+ }
+
+ return classes.join(" ");
+};
+
+/**
+ * This node represents a span node, with a className, a list of children, and
+ * an inline style. It also contains information about its height, depth, and
+ * maxFontSize.
+ */
+function span(classes, children, height, depth, maxFontSize, style) {
+ this.classes = classes || [];
+ this.children = children || [];
+ this.height = height || 0;
+ this.depth = depth || 0;
+ this.maxFontSize = maxFontSize || 0;
+ this.style = style || {};
+ this.attributes = {};
+}
+
+/**
+ * Sets an arbitrary attribute on the span. Warning: use this wisely. Not all
+ * browsers support attributes the same, and having too many custom attributes
+ * is probably bad.
+ */
+span.prototype.setAttribute = function(attribute, value) {
+ this.attributes[attribute] = value;
+};
+
+/**
+ * Convert the span into an HTML node
+ */
+span.prototype.toNode = function() {
+ var span = document.createElement("span");
+
+ // Apply the class
+ span.className = createClass(this.classes);
+
+ // Apply inline styles
+ for (var style in this.style) {
+ if (Object.prototype.hasOwnProperty.call(this.style, style)) {
+ span.style[style] = this.style[style];
+ }
+ }
+
+ // Apply attributes
+ for (var attr in this.attributes) {
+ if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
+ span.setAttribute(attr, this.attributes[attr]);
+ }
+ }
+
+ // Append the children, also as HTML nodes
+ for (var i = 0; i < this.children.length; i++) {
+ span.appendChild(this.children[i].toNode());
+ }
+
+ return span;
+};
+
+/**
+ * Convert the span into an HTML markup string
+ */
+span.prototype.toMarkup = function() {
+ var markup = "<span";
+
+ // Add the class
+ if (this.classes.length) {
+ markup += " class=\"";
+ markup += utils.escape(createClass(this.classes));
+ markup += "\"";
+ }
+
+ var styles = "";
+
+ // Add the styles, after hyphenation
+ for (var style in this.style) {
+ if (this.style.hasOwnProperty(style)) {
+ styles += utils.hyphenate(style) + ":" + this.style[style] + ";";
+ }
+ }
+
+ if (styles) {
+ markup += " style=\"" + utils.escape(styles) + "\"";
+ }
+
+ // Add the attributes
+ for (var attr in this.attributes) {
+ if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
+ markup += " " + attr + "=\"";
+ markup += utils.escape(this.attributes[attr]);
+ markup += "\"";
+ }
+ }
+
+ markup += ">";
+
+ // Add the markup of the children, also as markup
+ for (var i = 0; i < this.children.length; i++) {
+ markup += this.children[i].toMarkup();
+ }
+
+ markup += "</span>";
+
+ return markup;
+};
+
+/**
+ * This node represents a document fragment, which contains elements, but when
+ * placed into the DOM doesn't have any representation itself. Thus, it only
+ * contains children and doesn't have any HTML properties. It also keeps track
+ * of a height, depth, and maxFontSize.
+ */
+function documentFragment(children, height, depth, maxFontSize) {
+ this.children = children || [];
+ this.height = height || 0;
+ this.depth = depth || 0;
+ this.maxFontSize = maxFontSize || 0;
+}
+
+/**
+ * Convert the fragment into a node
+ */
+documentFragment.prototype.toNode = function() {
+ // Create a fragment
+ var frag = document.createDocumentFragment();
+
+ // Append the children
+ for (var i = 0; i < this.children.length; i++) {
+ frag.appendChild(this.children[i].toNode());
+ }
+
+ return frag;
+};
+
+/**
+ * Convert the fragment into HTML markup
+ */
+documentFragment.prototype.toMarkup = function() {
+ var markup = "";
+
+ // Simply concatenate the markup for the children together
+ for (var i = 0; i < this.children.length; i++) {
+ markup += this.children[i].toMarkup();
+ }
+
+ return markup;
+};
+
+var iCombinations = {
+ 'î': '\u0131\u0302',
+ 'ï': '\u0131\u0308',
+ 'í': '\u0131\u0301',
+ // 'ī': '\u0131\u0304', // enable when we add Extended Latin
+ 'ì': '\u0131\u0300',
+};
+
+/**
+ * A symbol node contains information about a single symbol. It either renders
+ * to a single text node, or a span with a single text node in it, depending on
+ * whether it has CSS classes, styles, or needs italic correction.
+ */
+function symbolNode(value, height, depth, italic, skew, classes, style) {
+ this.value = value || "";
+ this.height = height || 0;
+ this.depth = depth || 0;
+ this.italic = italic || 0;
+ this.skew = skew || 0;
+ this.classes = classes || [];
+ this.style = style || {};
+ this.maxFontSize = 0;
+
+ // Mark CJK characters with specific classes so that we can specify which
+ // fonts to use. This allows us to render these characters with a serif
+ // font in situations where the browser would either default to a sans serif
+ // or render a placeholder character.
+ if (unicodeRegexes.cjkRegex.test(value)) {
+ // I couldn't find any fonts that contained Hangul as well as all of
+ // the other characters we wanted to test there for it gets its own
+ // CSS class.
+ if (unicodeRegexes.hangulRegex.test(value)) {
+ this.classes.push('hangul_fallback');
+ } else {
+ this.classes.push('cjk_fallback');
+ }
+ }
+
+ if (/[îïíì]/.test(this.value)) { // add ī when we add Extended Latin
+ this.value = iCombinations[this.value];
+ }
+}
+
+/**
+ * Creates a text node or span from a symbol node. Note that a span is only
+ * created if it is needed.
+ */
+symbolNode.prototype.toNode = function() {
+ var node = document.createTextNode(this.value);
+ var span = null;
+
+ if (this.italic > 0) {
+ span = document.createElement("span");
+ span.style.marginRight = this.italic + "em";
+ }
+
+ if (this.classes.length > 0) {
+ span = span || document.createElement("span");
+ span.className = createClass(this.classes);
+ }
+
+ for (var style in this.style) {
+ if (this.style.hasOwnProperty(style)) {
+ span = span || document.createElement("span");
+ span.style[style] = this.style[style];
+ }
+ }
+
+ if (span) {
+ span.appendChild(node);
+ return span;
+ } else {
+ return node;
+ }
+};
+
+/**
+ * Creates markup for a symbol node.
+ */
+symbolNode.prototype.toMarkup = function() {
+ // TODO(alpert): More duplication than I'd like from
+ // span.prototype.toMarkup and symbolNode.prototype.toNode...
+ var needsSpan = false;
+
+ var markup = "<span";
+
+ if (this.classes.length) {
+ needsSpan = true;
+ markup += " class=\"";
+ markup += utils.escape(createClass(this.classes));
+ markup += "\"";
+ }
+
+ var styles = "";
+
+ if (this.italic > 0) {
+ styles += "margin-right:" + this.italic + "em;";
+ }
+ for (var style in this.style) {
+ if (this.style.hasOwnProperty(style)) {
+ styles += utils.hyphenate(style) + ":" + this.style[style] + ";";
+ }
+ }
+
+ if (styles) {
+ needsSpan = true;
+ markup += " style=\"" + utils.escape(styles) + "\"";
+ }
+
+ var escaped = utils.escape(this.value);
+ if (needsSpan) {
+ markup += ">";
+ markup += escaped;
+ markup += "</span>";
+ return markup;
+ } else {
+ return escaped;
+ }
+};
+
+module.exports = {
+ span: span,
+ documentFragment: documentFragment,
+ symbolNode: symbolNode,
+};
+
+},{"./unicodeRegexes":24,"./utils":25}],16:[function(require,module,exports){
+/* eslint no-constant-condition:0 */
+var fontMetrics = require("./fontMetrics");
+var parseData = require("./parseData");
+var ParseError = require("./ParseError");
+
+var ParseNode = parseData.ParseNode;
+
+/**
+ * Parse the body of the environment, with rows delimited by \\ and
+ * columns delimited by &, and create a nested list in row-major order
+ * with one group per cell.
+ */
+function parseArray(parser, result) {
+ var row = [];
+ var body = [row];
+ var rowGaps = [];
+ while (true) {
+ var cell = parser.parseExpression(false, null);
+ row.push(new ParseNode("ordgroup", cell, parser.mode));
+ var next = parser.nextToken.text;
+ if (next === "&") {
+ parser.consume();
+ } else if (next === "\\end") {
+ break;
+ } else if (next === "\\\\" || next === "\\cr") {
+ var cr = parser.parseFunction();
+ rowGaps.push(cr.value.size);
+ row = [];
+ body.push(row);
+ } else {
+ throw new ParseError("Expected & or \\\\ or \\end",
+ parser.nextToken);
+ }
+ }
+ result.body = body;
+ result.rowGaps = rowGaps;
+ return new ParseNode(result.type, result, parser.mode);
+}
+
+/*
+ * An environment definition is very similar to a function definition:
+ * it is declared with a name or a list of names, a set of properties
+ * and a handler containing the actual implementation.
+ *
+ * The properties include:
+ * - numArgs: The number of arguments after the \begin{name} function.
+ * - argTypes: (optional) Just like for a function
+ * - allowedInText: (optional) Whether or not the environment is allowed inside
+ * text mode (default false) (not enforced yet)
+ * - numOptionalArgs: (optional) Just like for a function
+ * A bare number instead of that object indicates the numArgs value.
+ *
+ * The handler function will receive two arguments
+ * - context: information and references provided by the parser
+ * - args: an array of arguments passed to \begin{name}
+ * The context contains the following properties:
+ * - envName: the name of the environment, one of the listed names.
+ * - parser: the parser object
+ * - lexer: the lexer object
+ * - positions: the positions associated with these arguments from args.
+ * The handler must return a ParseResult.
+ */
+
+function defineEnvironment(names, props, handler) {
+ if (typeof names === "string") {
+ names = [names];
+ }
+ if (typeof props === "number") {
+ props = { numArgs: props };
+ }
+ // Set default values of environments
+ var data = {
+ numArgs: props.numArgs || 0,
+ argTypes: props.argTypes,
+ greediness: 1,
+ allowedInText: !!props.allowedInText,
+ numOptionalArgs: props.numOptionalArgs || 0,
+ handler: handler,
+ };
+ for (var i = 0; i < names.length; ++i) {
+ module.exports[names[i]] = data;
+ }
+}
+
+// Arrays are part of LaTeX, defined in lttab.dtx so its documentation
+// is part of the source2e.pdf file of LaTeX2e source documentation.
+defineEnvironment("array", {
+ numArgs: 1,
+}, function(context, args) {
+ var colalign = args[0];
+ colalign = colalign.value.map ? colalign.value : [colalign];
+ var cols = colalign.map(function(node) {
+ var ca = node.value;
+ if ("lcr".indexOf(ca) !== -1) {
+ return {
+ type: "align",
+ align: ca,
+ };
+ } else if (ca === "|") {
+ return {
+ type: "separator",
+ separator: "|",
+ };
+ }
+ throw new ParseError(
+ "Unknown column alignment: " + node.value,
+ node);
+ });
+ var res = {
+ type: "array",
+ cols: cols,
+ hskipBeforeAndAfter: true, // \@preamble in lttab.dtx
+ };
+ res = parseArray(context.parser, res);
+ return res;
+});
+
+// The matrix environments of amsmath builds on the array environment
+// of LaTeX, which is discussed above.
+defineEnvironment([
+ "matrix",
+ "pmatrix",
+ "bmatrix",
+ "Bmatrix",
+ "vmatrix",
+ "Vmatrix",
+], {
+}, function(context) {
+ var delimiters = {
+ "matrix": null,
+ "pmatrix": ["(", ")"],
+ "bmatrix": ["[", "]"],
+ "Bmatrix": ["\\{", "\\}"],
+ "vmatrix": ["|", "|"],
+ "Vmatrix": ["\\Vert", "\\Vert"],
+ }[context.envName];
+ var res = {
+ type: "array",
+ hskipBeforeAndAfter: false, // \hskip -\arraycolsep in amsmath
+ };
+ res = parseArray(context.parser, res);
+ if (delimiters) {
+ res = new ParseNode("leftright", {
+ body: [res],
+ left: delimiters[0],
+ right: delimiters[1],
+ }, context.mode);
+ }
+ return res;
+});
+
+// A cases environment (in amsmath.sty) is almost equivalent to
+// \def\arraystretch{1.2}%
+// \left\{\begin{array}{@{}l@{\quad}l@{}} … \end{array}\right.
+defineEnvironment("cases", {
+}, function(context) {
+ var res = {
+ type: "array",
+ arraystretch: 1.2,
+ cols: [{
+ type: "align",
+ align: "l",
+ pregap: 0,
+ postgap: fontMetrics.metrics.quad,
+ }, {
+ type: "align",
+ align: "l",
+ pregap: 0,
+ postgap: 0,
+ }],
+ };
+ res = parseArray(context.parser, res);
+ res = new ParseNode("leftright", {
+ body: [res],
+ left: "\\{",
+ right: ".",
+ }, context.mode);
+ return res;
+});
+
+// An aligned environment is like the align* environment
+// except it operates within math mode.
+// Note that we assume \nomallineskiplimit to be zero,
+// so that \strut@ is the same as \strut.
+defineEnvironment("aligned", {
+}, function(context) {
+ var res = {
+ type: "array",
+ cols: [],
+ };
+ res = parseArray(context.parser, res);
+ var emptyGroup = new ParseNode("ordgroup", [], context.mode);
+ var numCols = 0;
+ res.value.body.forEach(function(row) {
+ var i;
+ for (i = 1; i < row.length; i += 2) {
+ row[i].value.unshift(emptyGroup);
+ }
+ if (numCols < row.length) {
+ numCols = row.length;
+ }
+ });
+ for (var i = 0; i < numCols; ++i) {
+ var align = "r";
+ var pregap = 0;
+ if (i % 2 === 1) {
+ align = "l";
+ } else if (i > 0) {
+ pregap = 2; // one \qquad between columns
+ }
+ res.value.cols[i] = {
+ type: "align",
+ align: align,
+ pregap: pregap,
+ postgap: 0,
+ };
+ }
+ return res;
+});
+
+},{"./ParseError":6,"./fontMetrics":17,"./parseData":21}],17:[function(require,module,exports){
+/* eslint no-unused-vars:0 */
+
+var Style = require("./Style");
+var cjkRegex = require("./unicodeRegexes").cjkRegex;
+
+/**
+ * This file contains metrics regarding fonts and individual symbols. The sigma
+ * and xi variables, as well as the metricMap map contain data extracted from
+ * TeX, TeX font metrics, and the TTF files. These data are then exposed via the
+ * `metrics` variable and the getCharacterMetrics function.
+ */
+
+// These font metrics are extracted from TeX by using
+// \font\a=cmmi10
+// \showthe\fontdimenX\a
+// where X is the corresponding variable number. These correspond to the font
+// parameters of the symbol fonts. In TeX, there are actually three sets of
+// dimensions, one for each of textstyle, scriptstyle, and scriptscriptstyle,
+// but we only use the textstyle ones, and scale certain dimensions accordingly.
+// See the TeXbook, page 441.
+var sigma1 = 0.025;
+var sigma2 = 0;
+var sigma3 = 0;
+var sigma4 = 0;
+var sigma5 = 0.431;
+var sigma6 = 1;
+var sigma7 = 0;
+var sigma8 = 0.677;
+var sigma9 = 0.394;
+var sigma10 = 0.444;
+var sigma11 = 0.686;
+var sigma12 = 0.345;
+var sigma13 = 0.413;
+var sigma14 = 0.363;
+var sigma15 = 0.289;
+var sigma16 = 0.150;
+var sigma17 = 0.247;
+var sigma18 = 0.386;
+var sigma19 = 0.050;
+var sigma20 = 2.390;
+var sigma21 = 1.01;
+var sigma21Script = 0.81;
+var sigma21ScriptScript = 0.71;
+var sigma22 = 0.250;
+
+// These font metrics are extracted from TeX by using
+// \font\a=cmex10
+// \showthe\fontdimenX\a
+// where X is the corresponding variable number. These correspond to the font
+// parameters of the extension fonts (family 3). See the TeXbook, page 441.
+var xi1 = 0;
+var xi2 = 0;
+var xi3 = 0;
+var xi4 = 0;
+var xi5 = 0.431;
+var xi6 = 1;
+var xi7 = 0;
+var xi8 = 0.04;
+var xi9 = 0.111;
+var xi10 = 0.166;
+var xi11 = 0.2;
+var xi12 = 0.6;
+var xi13 = 0.1;
+
+// This value determines how large a pt is, for metrics which are defined in
+// terms of pts.
+// This value is also used in katex.less; if you change it make sure the values
+// match.
+var ptPerEm = 10.0;
+
+// The space between adjacent `|` columns in an array definition. From
+// `\showthe\doublerulesep` in LaTeX.
+var doubleRuleSep = 2.0 / ptPerEm;
+
+/**
+ * This is just a mapping from common names to real metrics
+ */
+var metrics = {
+ xHeight: sigma5,
+ quad: sigma6,
+ num1: sigma8,
+ num2: sigma9,
+ num3: sigma10,
+ denom1: sigma11,
+ denom2: sigma12,
+ sup1: sigma13,
+ sup2: sigma14,
+ sup3: sigma15,
+ sub1: sigma16,
+ sub2: sigma17,
+ supDrop: sigma18,
+ subDrop: sigma19,
+ axisHeight: sigma22,
+ defaultRuleThickness: xi8,
+ bigOpSpacing1: xi9,
+ bigOpSpacing2: xi10,
+ bigOpSpacing3: xi11,
+ bigOpSpacing4: xi12,
+ bigOpSpacing5: xi13,
+ ptPerEm: ptPerEm,
+ emPerEx: sigma5 / sigma6,
+ doubleRuleSep: doubleRuleSep,
+
+ // TODO(alpert): Missing parallel structure here. We should probably add
+ // style-specific metrics for all of these.
+ delim1: sigma20,
+ getDelim2: function(style) {
+ if (style.size === Style.TEXT.size) {
+ return sigma21;
+ } else if (style.size === Style.SCRIPT.size) {
+ return sigma21Script;
+ } else if (style.size === Style.SCRIPTSCRIPT.size) {
+ return sigma21ScriptScript;
+ }
+ throw new Error("Unexpected style size: " + style.size);
+ },
+};
+
+// This map contains a mapping from font name and character code to character
+// metrics, including height, depth, italic correction, and skew (kern from the
+// character to the corresponding \skewchar)
+// This map is generated via `make metrics`. It should not be changed manually.
+var metricMap = require("./fontMetricsData");
+
+// These are very rough approximations. We default to Times New Roman which
+// should have Latin-1 and Cyrillic characters, but may not depending on the
+// operating system. The metrics do not account for extra height from the
+// accents. In the case of Cyrillic characters which have both ascenders and
+// descenders we prefer approximations with ascenders, primarily to prevent
+// the fraction bar or root line from intersecting the glyph.
+// TODO(kevinb) allow union of multiple glyph metrics for better accuracy.
+var extraCharacterMap = {
+ // Latin-1
+ 'À': 'A',
+ 'Á': 'A',
+ 'Â': 'A',
+ 'Ã': 'A',
+ 'Ä': 'A',
+ 'Å': 'A',
+ 'Æ': 'A',
+ 'Ç': 'C',
+ 'È': 'E',
+ 'É': 'E',
+ 'Ê': 'E',
+ 'Ë': 'E',
+ 'Ì': 'I',
+ 'Í': 'I',
+ 'Î': 'I',
+ 'Ï': 'I',
+ 'Ð': 'D',
+ 'Ñ': 'N',
+ 'Ò': 'O',
+ 'Ó': 'O',
+ 'Ô': 'O',
+ 'Õ': 'O',
+ 'Ö': 'O',
+ 'Ø': 'O',
+ 'Ù': 'U',
+ 'Ú': 'U',
+ 'Û': 'U',
+ 'Ü': 'U',
+ 'Ý': 'Y',
+ 'Þ': 'o',
+ 'ß': 'B',
+ 'à': 'a',
+ 'á': 'a',
+ 'â': 'a',
+ 'ã': 'a',
+ 'ä': 'a',
+ 'å': 'a',
+ 'æ': 'a',
+ 'ç': 'c',
+ 'è': 'e',
+ 'é': 'e',
+ 'ê': 'e',
+ 'ë': 'e',
+ 'ì': 'i',
+ 'í': 'i',
+ 'î': 'i',
+ 'ï': 'i',
+ 'ð': 'd',
+ 'ñ': 'n',
+ 'ò': 'o',
+ 'ó': 'o',
+ 'ô': 'o',
+ 'õ': 'o',
+ 'ö': 'o',
+ 'ø': 'o',
+ 'ù': 'u',
+ 'ú': 'u',
+ 'û': 'u',
+ 'ü': 'u',
+ 'ý': 'y',
+ 'þ': 'o',
+ 'ÿ': 'y',
+
+ // Cyrillic
+ 'А': 'A',
+ 'Б': 'B',
+ 'В': 'B',
+ 'Г': 'F',
+ 'Д': 'A',
+ 'Е': 'E',
+ 'Ж': 'K',
+ 'З': '3',
+ 'И': 'N',
+ 'Й': 'N',
+ 'К': 'K',
+ 'Л': 'N',
+ 'М': 'M',
+ 'Н': 'H',
+ 'О': 'O',
+ 'П': 'N',
+ 'Р': 'P',
+ 'С': 'C',
+ 'Т': 'T',
+ 'У': 'y',
+ 'Ф': 'O',
+ 'Х': 'X',
+ 'Ц': 'U',
+ 'Ч': 'h',
+ 'Ш': 'W',
+ 'Щ': 'W',
+ 'Ъ': 'B',
+ 'Ы': 'X',
+ 'Ь': 'B',
+ 'Э': '3',
+ 'Ю': 'X',
+ 'Я': 'R',
+ 'а': 'a',
+ 'б': 'b',
+ 'в': 'a',
+ 'г': 'r',
+ 'д': 'y',
+ 'е': 'e',
+ 'ж': 'm',
+ 'з': 'e',
+ 'и': 'n',
+ 'й': 'n',
+ 'к': 'n',
+ 'л': 'n',
+ 'м': 'm',
+ 'н': 'n',
+ 'о': 'o',
+ 'п': 'n',
+ 'р': 'p',
+ 'с': 'c',
+ 'т': 'o',
+ 'у': 'y',
+ 'ф': 'b',
+ 'х': 'x',
+ 'ц': 'n',
+ 'ч': 'n',
+ 'ш': 'w',
+ 'щ': 'w',
+ 'ъ': 'a',
+ 'ы': 'm',
+ 'ь': 'a',
+ 'э': 'e',
+ 'ю': 'm',
+ 'я': 'r',
+};
+
+/**
+ * This function is a convenience function for looking up information in the
+ * metricMap table. It takes a character as a string, and a style.
+ *
+ * Note: the `width` property may be undefined if fontMetricsData.js wasn't
+ * built using `Make extended_metrics`.
+ */
+var getCharacterMetrics = function(character, style) {
+ var ch = character.charCodeAt(0);
+ if (character[0] in extraCharacterMap) {
+ ch = extraCharacterMap[character[0]].charCodeAt(0);
+ } else if (cjkRegex.test(character[0])) {
+ ch = 'M'.charCodeAt(0);
+ }
+ var metrics = metricMap[style][ch];
+ if (metrics) {
+ return {
+ depth: metrics[0],
+ height: metrics[1],
+ italic: metrics[2],
+ skew: metrics[3],
+ width: metrics[4],
+ };
+ }
+};
+
+module.exports = {
+ metrics: metrics,
+ getCharacterMetrics: getCharacterMetrics,
+};
+
+},{"./Style":9,"./fontMetricsData":18,"./unicodeRegexes":24}],18:[function(require,module,exports){
+module.exports = {
+ "AMS-Regular": {
+ "65": [0, 0.68889, 0, 0],
+ "66": [0, 0.68889, 0, 0],
+ "67": [0, 0.68889, 0, 0],
+ "68": [0, 0.68889, 0, 0],
+ "69": [0, 0.68889, 0, 0],
+ "70": [0, 0.68889, 0, 0],
+ "71": [0, 0.68889, 0, 0],
+ "72": [0, 0.68889, 0, 0],
+ "73": [0, 0.68889, 0, 0],
+ "74": [0.16667, 0.68889, 0, 0],
+ "75": [0, 0.68889, 0, 0],
+ "76": [0, 0.68889, 0, 0],
+ "77": [0, 0.68889, 0, 0],
+ "78": [0, 0.68889, 0, 0],
+ "79": [0.16667, 0.68889, 0, 0],
+ "80": [0, 0.68889, 0, 0],
+ "81": [0.16667, 0.68889, 0, 0],
+ "82": [0, 0.68889, 0, 0],
+ "83": [0, 0.68889, 0, 0],
+ "84": [0, 0.68889, 0, 0],
+ "85": [0, 0.68889, 0, 0],
+ "86": [0, 0.68889, 0, 0],
+ "87": [0, 0.68889, 0, 0],
+ "88": [0, 0.68889, 0, 0],
+ "89": [0, 0.68889, 0, 0],
+ "90": [0, 0.68889, 0, 0],
+ "107": [0, 0.68889, 0, 0],
+ "165": [0, 0.675, 0.025, 0],
+ "174": [0.15559, 0.69224, 0, 0],
+ "240": [0, 0.68889, 0, 0],
+ "295": [0, 0.68889, 0, 0],
+ "710": [0, 0.825, 0, 0],
+ "732": [0, 0.9, 0, 0],
+ "770": [0, 0.825, 0, 0],
+ "771": [0, 0.9, 0, 0],
+ "989": [0.08167, 0.58167, 0, 0],
+ "1008": [0, 0.43056, 0.04028, 0],
+ "8245": [0, 0.54986, 0, 0],
+ "8463": [0, 0.68889, 0, 0],
+ "8487": [0, 0.68889, 0, 0],
+ "8498": [0, 0.68889, 0, 0],
+ "8502": [0, 0.68889, 0, 0],
+ "8503": [0, 0.68889, 0, 0],
+ "8504": [0, 0.68889, 0, 0],
+ "8513": [0, 0.68889, 0, 0],
+ "8592": [-0.03598, 0.46402, 0, 0],
+ "8594": [-0.03598, 0.46402, 0, 0],
+ "8602": [-0.13313, 0.36687, 0, 0],
+ "8603": [-0.13313, 0.36687, 0, 0],
+ "8606": [0.01354, 0.52239, 0, 0],
+ "8608": [0.01354, 0.52239, 0, 0],
+ "8610": [0.01354, 0.52239, 0, 0],
+ "8611": [0.01354, 0.52239, 0, 0],
+ "8619": [0, 0.54986, 0, 0],
+ "8620": [0, 0.54986, 0, 0],
+ "8621": [-0.13313, 0.37788, 0, 0],
+ "8622": [-0.13313, 0.36687, 0, 0],
+ "8624": [0, 0.69224, 0, 0],
+ "8625": [0, 0.69224, 0, 0],
+ "8630": [0, 0.43056, 0, 0],
+ "8631": [0, 0.43056, 0, 0],
+ "8634": [0.08198, 0.58198, 0, 0],
+ "8635": [0.08198, 0.58198, 0, 0],
+ "8638": [0.19444, 0.69224, 0, 0],
+ "8639": [0.19444, 0.69224, 0, 0],
+ "8642": [0.19444, 0.69224, 0, 0],
+ "8643": [0.19444, 0.69224, 0, 0],
+ "8644": [0.1808, 0.675, 0, 0],
+ "8646": [0.1808, 0.675, 0, 0],
+ "8647": [0.1808, 0.675, 0, 0],
+ "8648": [0.19444, 0.69224, 0, 0],
+ "8649": [0.1808, 0.675, 0, 0],
+ "8650": [0.19444, 0.69224, 0, 0],
+ "8651": [0.01354, 0.52239, 0, 0],
+ "8652": [0.01354, 0.52239, 0, 0],
+ "8653": [-0.13313, 0.36687, 0, 0],
+ "8654": [-0.13313, 0.36687, 0, 0],
+ "8655": [-0.13313, 0.36687, 0, 0],
+ "8666": [0.13667, 0.63667, 0, 0],
+ "8667": [0.13667, 0.63667, 0, 0],
+ "8669": [-0.13313, 0.37788, 0, 0],
+ "8672": [-0.064, 0.437, 0, 0],
+ "8674": [-0.064, 0.437, 0, 0],
+ "8705": [0, 0.825, 0, 0],
+ "8708": [0, 0.68889, 0, 0],
+ "8709": [0.08167, 0.58167, 0, 0],
+ "8717": [0, 0.43056, 0, 0],
+ "8722": [-0.03598, 0.46402, 0, 0],
+ "8724": [0.08198, 0.69224, 0, 0],
+ "8726": [0.08167, 0.58167, 0, 0],
+ "8733": [0, 0.69224, 0, 0],
+ "8736": [0, 0.69224, 0, 0],
+ "8737": [0, 0.69224, 0, 0],
+ "8738": [0.03517, 0.52239, 0, 0],
+ "8739": [0.08167, 0.58167, 0, 0],
+ "8740": [0.25142, 0.74111, 0, 0],
+ "8741": [0.08167, 0.58167, 0, 0],
+ "8742": [0.25142, 0.74111, 0, 0],
+ "8756": [0, 0.69224, 0, 0],
+ "8757": [0, 0.69224, 0, 0],
+ "8764": [-0.13313, 0.36687, 0, 0],
+ "8765": [-0.13313, 0.37788, 0, 0],
+ "8769": [-0.13313, 0.36687, 0, 0],
+ "8770": [-0.03625, 0.46375, 0, 0],
+ "8774": [0.30274, 0.79383, 0, 0],
+ "8776": [-0.01688, 0.48312, 0, 0],
+ "8778": [0.08167, 0.58167, 0, 0],
+ "8782": [0.06062, 0.54986, 0, 0],
+ "8783": [0.06062, 0.54986, 0, 0],
+ "8785": [0.08198, 0.58198, 0, 0],
+ "8786": [0.08198, 0.58198, 0, 0],
+ "8787": [0.08198, 0.58198, 0, 0],
+ "8790": [0, 0.69224, 0, 0],
+ "8791": [0.22958, 0.72958, 0, 0],
+ "8796": [0.08198, 0.91667, 0, 0],
+ "8806": [0.25583, 0.75583, 0, 0],
+ "8807": [0.25583, 0.75583, 0, 0],
+ "8808": [0.25142, 0.75726, 0, 0],
+ "8809": [0.25142, 0.75726, 0, 0],
+ "8812": [0.25583, 0.75583, 0, 0],
+ "8814": [0.20576, 0.70576, 0, 0],
+ "8815": [0.20576, 0.70576, 0, 0],
+ "8816": [0.30274, 0.79383, 0, 0],
+ "8817": [0.30274, 0.79383, 0, 0],
+ "8818": [0.22958, 0.72958, 0, 0],
+ "8819": [0.22958, 0.72958, 0, 0],
+ "8822": [0.1808, 0.675, 0, 0],
+ "8823": [0.1808, 0.675, 0, 0],
+ "8828": [0.13667, 0.63667, 0, 0],
+ "8829": [0.13667, 0.63667, 0, 0],
+ "8830": [0.22958, 0.72958, 0, 0],
+ "8831": [0.22958, 0.72958, 0, 0],
+ "8832": [0.20576, 0.70576, 0, 0],
+ "8833": [0.20576, 0.70576, 0, 0],
+ "8840": [0.30274, 0.79383, 0, 0],
+ "8841": [0.30274, 0.79383, 0, 0],
+ "8842": [0.13597, 0.63597, 0, 0],
+ "8843": [0.13597, 0.63597, 0, 0],
+ "8847": [0.03517, 0.54986, 0, 0],
+ "8848": [0.03517, 0.54986, 0, 0],
+ "8858": [0.08198, 0.58198, 0, 0],
+ "8859": [0.08198, 0.58198, 0, 0],
+ "8861": [0.08198, 0.58198, 0, 0],
+ "8862": [0, 0.675, 0, 0],
+ "8863": [0, 0.675, 0, 0],
+ "8864": [0, 0.675, 0, 0],
+ "8865": [0, 0.675, 0, 0],
+ "8872": [0, 0.69224, 0, 0],
+ "8873": [0, 0.69224, 0, 0],
+ "8874": [0, 0.69224, 0, 0],
+ "8876": [0, 0.68889, 0, 0],
+ "8877": [0, 0.68889, 0, 0],
+ "8878": [0, 0.68889, 0, 0],
+ "8879": [0, 0.68889, 0, 0],
+ "8882": [0.03517, 0.54986, 0, 0],
+ "8883": [0.03517, 0.54986, 0, 0],
+ "8884": [0.13667, 0.63667, 0, 0],
+ "8885": [0.13667, 0.63667, 0, 0],
+ "8888": [0, 0.54986, 0, 0],
+ "8890": [0.19444, 0.43056, 0, 0],
+ "8891": [0.19444, 0.69224, 0, 0],
+ "8892": [0.19444, 0.69224, 0, 0],
+ "8901": [0, 0.54986, 0, 0],
+ "8903": [0.08167, 0.58167, 0, 0],
+ "8905": [0.08167, 0.58167, 0, 0],
+ "8906": [0.08167, 0.58167, 0, 0],
+ "8907": [0, 0.69224, 0, 0],
+ "8908": [0, 0.69224, 0, 0],
+ "8909": [-0.03598, 0.46402, 0, 0],
+ "8910": [0, 0.54986, 0, 0],
+ "8911": [0, 0.54986, 0, 0],
+ "8912": [0.03517, 0.54986, 0, 0],
+ "8913": [0.03517, 0.54986, 0, 0],
+ "8914": [0, 0.54986, 0, 0],
+ "8915": [0, 0.54986, 0, 0],
+ "8916": [0, 0.69224, 0, 0],
+ "8918": [0.0391, 0.5391, 0, 0],
+ "8919": [0.0391, 0.5391, 0, 0],
+ "8920": [0.03517, 0.54986, 0, 0],
+ "8921": [0.03517, 0.54986, 0, 0],
+ "8922": [0.38569, 0.88569, 0, 0],
+ "8923": [0.38569, 0.88569, 0, 0],
+ "8926": [0.13667, 0.63667, 0, 0],
+ "8927": [0.13667, 0.63667, 0, 0],
+ "8928": [0.30274, 0.79383, 0, 0],
+ "8929": [0.30274, 0.79383, 0, 0],
+ "8934": [0.23222, 0.74111, 0, 0],
+ "8935": [0.23222, 0.74111, 0, 0],
+ "8936": [0.23222, 0.74111, 0, 0],
+ "8937": [0.23222, 0.74111, 0, 0],
+ "8938": [0.20576, 0.70576, 0, 0],
+ "8939": [0.20576, 0.70576, 0, 0],
+ "8940": [0.30274, 0.79383, 0, 0],
+ "8941": [0.30274, 0.79383, 0, 0],
+ "8994": [0.19444, 0.69224, 0, 0],
+ "8995": [0.19444, 0.69224, 0, 0],
+ "9416": [0.15559, 0.69224, 0, 0],
+ "9484": [0, 0.69224, 0, 0],
+ "9488": [0, 0.69224, 0, 0],
+ "9492": [0, 0.37788, 0, 0],
+ "9496": [0, 0.37788, 0, 0],
+ "9585": [0.19444, 0.68889, 0, 0],
+ "9586": [0.19444, 0.74111, 0, 0],
+ "9632": [0, 0.675, 0, 0],
+ "9633": [0, 0.675, 0, 0],
+ "9650": [0, 0.54986, 0, 0],
+ "9651": [0, 0.54986, 0, 0],
+ "9654": [0.03517, 0.54986, 0, 0],
+ "9660": [0, 0.54986, 0, 0],
+ "9661": [0, 0.54986, 0, 0],
+ "9664": [0.03517, 0.54986, 0, 0],
+ "9674": [0.11111, 0.69224, 0, 0],
+ "9733": [0.19444, 0.69224, 0, 0],
+ "10003": [0, 0.69224, 0, 0],
+ "10016": [0, 0.69224, 0, 0],
+ "10731": [0.11111, 0.69224, 0, 0],
+ "10846": [0.19444, 0.75583, 0, 0],
+ "10877": [0.13667, 0.63667, 0, 0],
+ "10878": [0.13667, 0.63667, 0, 0],
+ "10885": [0.25583, 0.75583, 0, 0],
+ "10886": [0.25583, 0.75583, 0, 0],
+ "10887": [0.13597, 0.63597, 0, 0],
+ "10888": [0.13597, 0.63597, 0, 0],
+ "10889": [0.26167, 0.75726, 0, 0],
+ "10890": [0.26167, 0.75726, 0, 0],
+ "10891": [0.48256, 0.98256, 0, 0],
+ "10892": [0.48256, 0.98256, 0, 0],
+ "10901": [0.13667, 0.63667, 0, 0],
+ "10902": [0.13667, 0.63667, 0, 0],
+ "10933": [0.25142, 0.75726, 0, 0],
+ "10934": [0.25142, 0.75726, 0, 0],
+ "10935": [0.26167, 0.75726, 0, 0],
+ "10936": [0.26167, 0.75726, 0, 0],
+ "10937": [0.26167, 0.75726, 0, 0],
+ "10938": [0.26167, 0.75726, 0, 0],
+ "10949": [0.25583, 0.75583, 0, 0],
+ "10950": [0.25583, 0.75583, 0, 0],
+ "10955": [0.28481, 0.79383, 0, 0],
+ "10956": [0.28481, 0.79383, 0, 0],
+ "57350": [0.08167, 0.58167, 0, 0],
+ "57351": [0.08167, 0.58167, 0, 0],
+ "57352": [0.08167, 0.58167, 0, 0],
+ "57353": [0, 0.43056, 0.04028, 0],
+ "57356": [0.25142, 0.75726, 0, 0],
+ "57357": [0.25142, 0.75726, 0, 0],
+ "57358": [0.41951, 0.91951, 0, 0],
+ "57359": [0.30274, 0.79383, 0, 0],
+ "57360": [0.30274, 0.79383, 0, 0],
+ "57361": [0.41951, 0.91951, 0, 0],
+ "57366": [0.25142, 0.75726, 0, 0],
+ "57367": [0.25142, 0.75726, 0, 0],
+ "57368": [0.25142, 0.75726, 0, 0],
+ "57369": [0.25142, 0.75726, 0, 0],
+ "57370": [0.13597, 0.63597, 0, 0],
+ "57371": [0.13597, 0.63597, 0, 0],
+ },
+ "Caligraphic-Regular": {
+ "48": [0, 0.43056, 0, 0],
+ "49": [0, 0.43056, 0, 0],
+ "50": [0, 0.43056, 0, 0],
+ "51": [0.19444, 0.43056, 0, 0],
+ "52": [0.19444, 0.43056, 0, 0],
+ "53": [0.19444, 0.43056, 0, 0],
+ "54": [0, 0.64444, 0, 0],
+ "55": [0.19444, 0.43056, 0, 0],
+ "56": [0, 0.64444, 0, 0],
+ "57": [0.19444, 0.43056, 0, 0],
+ "65": [0, 0.68333, 0, 0.19445],
+ "66": [0, 0.68333, 0.03041, 0.13889],
+ "67": [0, 0.68333, 0.05834, 0.13889],
+ "68": [0, 0.68333, 0.02778, 0.08334],
+ "69": [0, 0.68333, 0.08944, 0.11111],
+ "70": [0, 0.68333, 0.09931, 0.11111],
+ "71": [0.09722, 0.68333, 0.0593, 0.11111],
+ "72": [0, 0.68333, 0.00965, 0.11111],
+ "73": [0, 0.68333, 0.07382, 0],
+ "74": [0.09722, 0.68333, 0.18472, 0.16667],
+ "75": [0, 0.68333, 0.01445, 0.05556],
+ "76": [0, 0.68333, 0, 0.13889],
+ "77": [0, 0.68333, 0, 0.13889],
+ "78": [0, 0.68333, 0.14736, 0.08334],
+ "79": [0, 0.68333, 0.02778, 0.11111],
+ "80": [0, 0.68333, 0.08222, 0.08334],
+ "81": [0.09722, 0.68333, 0, 0.11111],
+ "82": [0, 0.68333, 0, 0.08334],
+ "83": [0, 0.68333, 0.075, 0.13889],
+ "84": [0, 0.68333, 0.25417, 0],
+ "85": [0, 0.68333, 0.09931, 0.08334],
+ "86": [0, 0.68333, 0.08222, 0],
+ "87": [0, 0.68333, 0.08222, 0.08334],
+ "88": [0, 0.68333, 0.14643, 0.13889],
+ "89": [0.09722, 0.68333, 0.08222, 0.08334],
+ "90": [0, 0.68333, 0.07944, 0.13889],
+ },
+ "Fraktur-Regular": {
+ "33": [0, 0.69141, 0, 0],
+ "34": [0, 0.69141, 0, 0],
+ "38": [0, 0.69141, 0, 0],
+ "39": [0, 0.69141, 0, 0],
+ "40": [0.24982, 0.74947, 0, 0],
+ "41": [0.24982, 0.74947, 0, 0],
+ "42": [0, 0.62119, 0, 0],
+ "43": [0.08319, 0.58283, 0, 0],
+ "44": [0, 0.10803, 0, 0],
+ "45": [0.08319, 0.58283, 0, 0],
+ "46": [0, 0.10803, 0, 0],
+ "47": [0.24982, 0.74947, 0, 0],
+ "48": [0, 0.47534, 0, 0],
+ "49": [0, 0.47534, 0, 0],
+ "50": [0, 0.47534, 0, 0],
+ "51": [0.18906, 0.47534, 0, 0],
+ "52": [0.18906, 0.47534, 0, 0],
+ "53": [0.18906, 0.47534, 0, 0],
+ "54": [0, 0.69141, 0, 0],
+ "55": [0.18906, 0.47534, 0, 0],
+ "56": [0, 0.69141, 0, 0],
+ "57": [0.18906, 0.47534, 0, 0],
+ "58": [0, 0.47534, 0, 0],
+ "59": [0.12604, 0.47534, 0, 0],
+ "61": [-0.13099, 0.36866, 0, 0],
+ "63": [0, 0.69141, 0, 0],
+ "65": [0, 0.69141, 0, 0],
+ "66": [0, 0.69141, 0, 0],
+ "67": [0, 0.69141, 0, 0],
+ "68": [0, 0.69141, 0, 0],
+ "69": [0, 0.69141, 0, 0],
+ "70": [0.12604, 0.69141, 0, 0],
+ "71": [0, 0.69141, 0, 0],
+ "72": [0.06302, 0.69141, 0, 0],
+ "73": [0, 0.69141, 0, 0],
+ "74": [0.12604, 0.69141, 0, 0],
+ "75": [0, 0.69141, 0, 0],
+ "76": [0, 0.69141, 0, 0],
+ "77": [0, 0.69141, 0, 0],
+ "78": [0, 0.69141, 0, 0],
+ "79": [0, 0.69141, 0, 0],
+ "80": [0.18906, 0.69141, 0, 0],
+ "81": [0.03781, 0.69141, 0, 0],
+ "82": [0, 0.69141, 0, 0],
+ "83": [0, 0.69141, 0, 0],
+ "84": [0, 0.69141, 0, 0],
+ "85": [0, 0.69141, 0, 0],
+ "86": [0, 0.69141, 0, 0],
+ "87": [0, 0.69141, 0, 0],
+ "88": [0, 0.69141, 0, 0],
+ "89": [0.18906, 0.69141, 0, 0],
+ "90": [0.12604, 0.69141, 0, 0],
+ "91": [0.24982, 0.74947, 0, 0],
+ "93": [0.24982, 0.74947, 0, 0],
+ "94": [0, 0.69141, 0, 0],
+ "97": [0, 0.47534, 0, 0],
+ "98": [0, 0.69141, 0, 0],
+ "99": [0, 0.47534, 0, 0],
+ "100": [0, 0.62119, 0, 0],
+ "101": [0, 0.47534, 0, 0],
+ "102": [0.18906, 0.69141, 0, 0],
+ "103": [0.18906, 0.47534, 0, 0],
+ "104": [0.18906, 0.69141, 0, 0],
+ "105": [0, 0.69141, 0, 0],
+ "106": [0, 0.69141, 0, 0],
+ "107": [0, 0.69141, 0, 0],
+ "108": [0, 0.69141, 0, 0],
+ "109": [0, 0.47534, 0, 0],
+ "110": [0, 0.47534, 0, 0],
+ "111": [0, 0.47534, 0, 0],
+ "112": [0.18906, 0.52396, 0, 0],
+ "113": [0.18906, 0.47534, 0, 0],
+ "114": [0, 0.47534, 0, 0],
+ "115": [0, 0.47534, 0, 0],
+ "116": [0, 0.62119, 0, 0],
+ "117": [0, 0.47534, 0, 0],
+ "118": [0, 0.52396, 0, 0],
+ "119": [0, 0.52396, 0, 0],
+ "120": [0.18906, 0.47534, 0, 0],
+ "121": [0.18906, 0.47534, 0, 0],
+ "122": [0.18906, 0.47534, 0, 0],
+ "8216": [0, 0.69141, 0, 0],
+ "8217": [0, 0.69141, 0, 0],
+ "58112": [0, 0.62119, 0, 0],
+ "58113": [0, 0.62119, 0, 0],
+ "58114": [0.18906, 0.69141, 0, 0],
+ "58115": [0.18906, 0.69141, 0, 0],
+ "58116": [0.18906, 0.47534, 0, 0],
+ "58117": [0, 0.69141, 0, 0],
+ "58118": [0, 0.62119, 0, 0],
+ "58119": [0, 0.47534, 0, 0],
+ },
+ "Main-Bold": {
+ "33": [0, 0.69444, 0, 0],
+ "34": [0, 0.69444, 0, 0],
+ "35": [0.19444, 0.69444, 0, 0],
+ "36": [0.05556, 0.75, 0, 0],
+ "37": [0.05556, 0.75, 0, 0],
+ "38": [0, 0.69444, 0, 0],
+ "39": [0, 0.69444, 0, 0],
+ "40": [0.25, 0.75, 0, 0],
+ "41": [0.25, 0.75, 0, 0],
+ "42": [0, 0.75, 0, 0],
+ "43": [0.13333, 0.63333, 0, 0],
+ "44": [0.19444, 0.15556, 0, 0],
+ "45": [0, 0.44444, 0, 0],
+ "46": [0, 0.15556, 0, 0],
+ "47": [0.25, 0.75, 0, 0],
+ "48": [0, 0.64444, 0, 0],
+ "49": [0, 0.64444, 0, 0],
+ "50": [0, 0.64444, 0, 0],
+ "51": [0, 0.64444, 0, 0],
+ "52": [0, 0.64444, 0, 0],
+ "53": [0, 0.64444, 0, 0],
+ "54": [0, 0.64444, 0, 0],
+ "55": [0, 0.64444, 0, 0],
+ "56": [0, 0.64444, 0, 0],
+ "57": [0, 0.64444, 0, 0],
+ "58": [0, 0.44444, 0, 0],
+ "59": [0.19444, 0.44444, 0, 0],
+ "60": [0.08556, 0.58556, 0, 0],
+ "61": [-0.10889, 0.39111, 0, 0],
+ "62": [0.08556, 0.58556, 0, 0],
+ "63": [0, 0.69444, 0, 0],
+ "64": [0, 0.69444, 0, 0],
+ "65": [0, 0.68611, 0, 0],
+ "66": [0, 0.68611, 0, 0],
+ "67": [0, 0.68611, 0, 0],
+ "68": [0, 0.68611, 0, 0],
+ "69": [0, 0.68611, 0, 0],
+ "70": [0, 0.68611, 0, 0],
+ "71": [0, 0.68611, 0, 0],
+ "72": [0, 0.68611, 0, 0],
+ "73": [0, 0.68611, 0, 0],
+ "74": [0, 0.68611, 0, 0],
+ "75": [0, 0.68611, 0, 0],
+ "76": [0, 0.68611, 0, 0],
+ "77": [0, 0.68611, 0, 0],
+ "78": [0, 0.68611, 0, 0],
+ "79": [0, 0.68611, 0, 0],
+ "80": [0, 0.68611, 0, 0],
+ "81": [0.19444, 0.68611, 0, 0],
+ "82": [0, 0.68611, 0, 0],
+ "83": [0, 0.68611, 0, 0],
+ "84": [0, 0.68611, 0, 0],
+ "85": [0, 0.68611, 0, 0],
+ "86": [0, 0.68611, 0.01597, 0],
+ "87": [0, 0.68611, 0.01597, 0],
+ "88": [0, 0.68611, 0, 0],
+ "89": [0, 0.68611, 0.02875, 0],
+ "90": [0, 0.68611, 0, 0],
+ "91": [0.25, 0.75, 0, 0],
+ "92": [0.25, 0.75, 0, 0],
+ "93": [0.25, 0.75, 0, 0],
+ "94": [0, 0.69444, 0, 0],
+ "95": [0.31, 0.13444, 0.03194, 0],
+ "96": [0, 0.69444, 0, 0],
+ "97": [0, 0.44444, 0, 0],
+ "98": [0, 0.69444, 0, 0],
+ "99": [0, 0.44444, 0, 0],
+ "100": [0, 0.69444, 0, 0],
+ "101": [0, 0.44444, 0, 0],
+ "102": [0, 0.69444, 0.10903, 0],
+ "103": [0.19444, 0.44444, 0.01597, 0],
+ "104": [0, 0.69444, 0, 0],
+ "105": [0, 0.69444, 0, 0],
+ "106": [0.19444, 0.69444, 0, 0],
+ "107": [0, 0.69444, 0, 0],
+ "108": [0, 0.69444, 0, 0],
+ "109": [0, 0.44444, 0, 0],
+ "110": [0, 0.44444, 0, 0],
+ "111": [0, 0.44444, 0, 0],
+ "112": [0.19444, 0.44444, 0, 0],
+ "113": [0.19444, 0.44444, 0, 0],
+ "114": [0, 0.44444, 0, 0],
+ "115": [0, 0.44444, 0, 0],
+ "116": [0, 0.63492, 0, 0],
+ "117": [0, 0.44444, 0, 0],
+ "118": [0, 0.44444, 0.01597, 0],
+ "119": [0, 0.44444, 0.01597, 0],
+ "120": [0, 0.44444, 0, 0],
+ "121": [0.19444, 0.44444, 0.01597, 0],
+ "122": [0, 0.44444, 0, 0],
+ "123": [0.25, 0.75, 0, 0],
+ "124": [0.25, 0.75, 0, 0],
+ "125": [0.25, 0.75, 0, 0],
+ "126": [0.35, 0.34444, 0, 0],
+ "168": [0, 0.69444, 0, 0],
+ "172": [0, 0.44444, 0, 0],
+ "175": [0, 0.59611, 0, 0],
+ "176": [0, 0.69444, 0, 0],
+ "177": [0.13333, 0.63333, 0, 0],
+ "180": [0, 0.69444, 0, 0],
+ "215": [0.13333, 0.63333, 0, 0],
+ "247": [0.13333, 0.63333, 0, 0],
+ "305": [0, 0.44444, 0, 0],
+ "567": [0.19444, 0.44444, 0, 0],
+ "710": [0, 0.69444, 0, 0],
+ "711": [0, 0.63194, 0, 0],
+ "713": [0, 0.59611, 0, 0],
+ "714": [0, 0.69444, 0, 0],
+ "715": [0, 0.69444, 0, 0],
+ "728": [0, 0.69444, 0, 0],
+ "729": [0, 0.69444, 0, 0],
+ "730": [0, 0.69444, 0, 0],
+ "732": [0, 0.69444, 0, 0],
+ "768": [0, 0.69444, 0, 0],
+ "769": [0, 0.69444, 0, 0],
+ "770": [0, 0.69444, 0, 0],
+ "771": [0, 0.69444, 0, 0],
+ "772": [0, 0.59611, 0, 0],
+ "774": [0, 0.69444, 0, 0],
+ "775": [0, 0.69444, 0, 0],
+ "776": [0, 0.69444, 0, 0],
+ "778": [0, 0.69444, 0, 0],
+ "779": [0, 0.69444, 0, 0],
+ "780": [0, 0.63194, 0, 0],
+ "824": [0.19444, 0.69444, 0, 0],
+ "915": [0, 0.68611, 0, 0],
+ "916": [0, 0.68611, 0, 0],
+ "920": [0, 0.68611, 0, 0],
+ "923": [0, 0.68611, 0, 0],
+ "926": [0, 0.68611, 0, 0],
+ "928": [0, 0.68611, 0, 0],
+ "931": [0, 0.68611, 0, 0],
+ "933": [0, 0.68611, 0, 0],
+ "934": [0, 0.68611, 0, 0],
+ "936": [0, 0.68611, 0, 0],
+ "937": [0, 0.68611, 0, 0],
+ "8211": [0, 0.44444, 0.03194, 0],
+ "8212": [0, 0.44444, 0.03194, 0],
+ "8216": [0, 0.69444, 0, 0],
+ "8217": [0, 0.69444, 0, 0],
+ "8220": [0, 0.69444, 0, 0],
+ "8221": [0, 0.69444, 0, 0],
+ "8224": [0.19444, 0.69444, 0, 0],
+ "8225": [0.19444, 0.69444, 0, 0],
+ "8242": [0, 0.55556, 0, 0],
+ "8407": [0, 0.72444, 0.15486, 0],
+ "8463": [0, 0.69444, 0, 0],
+ "8465": [0, 0.69444, 0, 0],
+ "8467": [0, 0.69444, 0, 0],
+ "8472": [0.19444, 0.44444, 0, 0],
+ "8476": [0, 0.69444, 0, 0],
+ "8501": [0, 0.69444, 0, 0],
+ "8592": [-0.10889, 0.39111, 0, 0],
+ "8593": [0.19444, 0.69444, 0, 0],
+ "8594": [-0.10889, 0.39111, 0, 0],
+ "8595": [0.19444, 0.69444, 0, 0],
+ "8596": [-0.10889, 0.39111, 0, 0],
+ "8597": [0.25, 0.75, 0, 0],
+ "8598": [0.19444, 0.69444, 0, 0],
+ "8599": [0.19444, 0.69444, 0, 0],
+ "8600": [0.19444, 0.69444, 0, 0],
+ "8601": [0.19444, 0.69444, 0, 0],
+ "8636": [-0.10889, 0.39111, 0, 0],
+ "8637": [-0.10889, 0.39111, 0, 0],
+ "8640": [-0.10889, 0.39111, 0, 0],
+ "8641": [-0.10889, 0.39111, 0, 0],
+ "8656": [-0.10889, 0.39111, 0, 0],
+ "8657": [0.19444, 0.69444, 0, 0],
+ "8658": [-0.10889, 0.39111, 0, 0],
+ "8659": [0.19444, 0.69444, 0, 0],
+ "8660": [-0.10889, 0.39111, 0, 0],
+ "8661": [0.25, 0.75, 0, 0],
+ "8704": [0, 0.69444, 0, 0],
+ "8706": [0, 0.69444, 0.06389, 0],
+ "8707": [0, 0.69444, 0, 0],
+ "8709": [0.05556, 0.75, 0, 0],
+ "8711": [0, 0.68611, 0, 0],
+ "8712": [0.08556, 0.58556, 0, 0],
+ "8715": [0.08556, 0.58556, 0, 0],
+ "8722": [0.13333, 0.63333, 0, 0],
+ "8723": [0.13333, 0.63333, 0, 0],
+ "8725": [0.25, 0.75, 0, 0],
+ "8726": [0.25, 0.75, 0, 0],
+ "8727": [-0.02778, 0.47222, 0, 0],
+ "8728": [-0.02639, 0.47361, 0, 0],
+ "8729": [-0.02639, 0.47361, 0, 0],
+ "8730": [0.18, 0.82, 0, 0],
+ "8733": [0, 0.44444, 0, 0],
+ "8734": [0, 0.44444, 0, 0],
+ "8736": [0, 0.69224, 0, 0],
+ "8739": [0.25, 0.75, 0, 0],
+ "8741": [0.25, 0.75, 0, 0],
+ "8743": [0, 0.55556, 0, 0],
+ "8744": [0, 0.55556, 0, 0],
+ "8745": [0, 0.55556, 0, 0],
+ "8746": [0, 0.55556, 0, 0],
+ "8747": [0.19444, 0.69444, 0.12778, 0],
+ "8764": [-0.10889, 0.39111, 0, 0],
+ "8768": [0.19444, 0.69444, 0, 0],
+ "8771": [0.00222, 0.50222, 0, 0],
+ "8776": [0.02444, 0.52444, 0, 0],
+ "8781": [0.00222, 0.50222, 0, 0],
+ "8801": [0.00222, 0.50222, 0, 0],
+ "8804": [0.19667, 0.69667, 0, 0],
+ "8805": [0.19667, 0.69667, 0, 0],
+ "8810": [0.08556, 0.58556, 0, 0],
+ "8811": [0.08556, 0.58556, 0, 0],
+ "8826": [0.08556, 0.58556, 0, 0],
+ "8827": [0.08556, 0.58556, 0, 0],
+ "8834": [0.08556, 0.58556, 0, 0],
+ "8835": [0.08556, 0.58556, 0, 0],
+ "8838": [0.19667, 0.69667, 0, 0],
+ "8839": [0.19667, 0.69667, 0, 0],
+ "8846": [0, 0.55556, 0, 0],
+ "8849": [0.19667, 0.69667, 0, 0],
+ "8850": [0.19667, 0.69667, 0, 0],
+ "8851": [0, 0.55556, 0, 0],
+ "8852": [0, 0.55556, 0, 0],
+ "8853": [0.13333, 0.63333, 0, 0],
+ "8854": [0.13333, 0.63333, 0, 0],
+ "8855": [0.13333, 0.63333, 0, 0],
+ "8856": [0.13333, 0.63333, 0, 0],
+ "8857": [0.13333, 0.63333, 0, 0],
+ "8866": [0, 0.69444, 0, 0],
+ "8867": [0, 0.69444, 0, 0],
+ "8868": [0, 0.69444, 0, 0],
+ "8869": [0, 0.69444, 0, 0],
+ "8900": [-0.02639, 0.47361, 0, 0],
+ "8901": [-0.02639, 0.47361, 0, 0],
+ "8902": [-0.02778, 0.47222, 0, 0],
+ "8968": [0.25, 0.75, 0, 0],
+ "8969": [0.25, 0.75, 0, 0],
+ "8970": [0.25, 0.75, 0, 0],
+ "8971": [0.25, 0.75, 0, 0],
+ "8994": [-0.13889, 0.36111, 0, 0],
+ "8995": [-0.13889, 0.36111, 0, 0],
+ "9651": [0.19444, 0.69444, 0, 0],
+ "9657": [-0.02778, 0.47222, 0, 0],
+ "9661": [0.19444, 0.69444, 0, 0],
+ "9667": [-0.02778, 0.47222, 0, 0],
+ "9711": [0.19444, 0.69444, 0, 0],
+ "9824": [0.12963, 0.69444, 0, 0],
+ "9825": [0.12963, 0.69444, 0, 0],
+ "9826": [0.12963, 0.69444, 0, 0],
+ "9827": [0.12963, 0.69444, 0, 0],
+ "9837": [0, 0.75, 0, 0],
+ "9838": [0.19444, 0.69444, 0, 0],
+ "9839": [0.19444, 0.69444, 0, 0],
+ "10216": [0.25, 0.75, 0, 0],
+ "10217": [0.25, 0.75, 0, 0],
+ "10815": [0, 0.68611, 0, 0],
+ "10927": [0.19667, 0.69667, 0, 0],
+ "10928": [0.19667, 0.69667, 0, 0],
+ },
+ "Main-Italic": {
+ "33": [0, 0.69444, 0.12417, 0],
+ "34": [0, 0.69444, 0.06961, 0],
+ "35": [0.19444, 0.69444, 0.06616, 0],
+ "37": [0.05556, 0.75, 0.13639, 0],
+ "38": [0, 0.69444, 0.09694, 0],
+ "39": [0, 0.69444, 0.12417, 0],
+ "40": [0.25, 0.75, 0.16194, 0],
+ "41": [0.25, 0.75, 0.03694, 0],
+ "42": [0, 0.75, 0.14917, 0],
+ "43": [0.05667, 0.56167, 0.03694, 0],
+ "44": [0.19444, 0.10556, 0, 0],
+ "45": [0, 0.43056, 0.02826, 0],
+ "46": [0, 0.10556, 0, 0],
+ "47": [0.25, 0.75, 0.16194, 0],
+ "48": [0, 0.64444, 0.13556, 0],
+ "49": [0, 0.64444, 0.13556, 0],
+ "50": [0, 0.64444, 0.13556, 0],
+ "51": [0, 0.64444, 0.13556, 0],
+ "52": [0.19444, 0.64444, 0.13556, 0],
+ "53": [0, 0.64444, 0.13556, 0],
+ "54": [0, 0.64444, 0.13556, 0],
+ "55": [0.19444, 0.64444, 0.13556, 0],
+ "56": [0, 0.64444, 0.13556, 0],
+ "57": [0, 0.64444, 0.13556, 0],
+ "58": [0, 0.43056, 0.0582, 0],
+ "59": [0.19444, 0.43056, 0.0582, 0],
+ "61": [-0.13313, 0.36687, 0.06616, 0],
+ "63": [0, 0.69444, 0.1225, 0],
+ "64": [0, 0.69444, 0.09597, 0],
+ "65": [0, 0.68333, 0, 0],
+ "66": [0, 0.68333, 0.10257, 0],
+ "67": [0, 0.68333, 0.14528, 0],
+ "68": [0, 0.68333, 0.09403, 0],
+ "69": [0, 0.68333, 0.12028, 0],
+ "70": [0, 0.68333, 0.13305, 0],
+ "71": [0, 0.68333, 0.08722, 0],
+ "72": [0, 0.68333, 0.16389, 0],
+ "73": [0, 0.68333, 0.15806, 0],
+ "74": [0, 0.68333, 0.14028, 0],
+ "75": [0, 0.68333, 0.14528, 0],
+ "76": [0, 0.68333, 0, 0],
+ "77": [0, 0.68333, 0.16389, 0],
+ "78": [0, 0.68333, 0.16389, 0],
+ "79": [0, 0.68333, 0.09403, 0],
+ "80": [0, 0.68333, 0.10257, 0],
+ "81": [0.19444, 0.68333, 0.09403, 0],
+ "82": [0, 0.68333, 0.03868, 0],
+ "83": [0, 0.68333, 0.11972, 0],
+ "84": [0, 0.68333, 0.13305, 0],
+ "85": [0, 0.68333, 0.16389, 0],
+ "86": [0, 0.68333, 0.18361, 0],
+ "87": [0, 0.68333, 0.18361, 0],
+ "88": [0, 0.68333, 0.15806, 0],
+ "89": [0, 0.68333, 0.19383, 0],
+ "90": [0, 0.68333, 0.14528, 0],
+ "91": [0.25, 0.75, 0.1875, 0],
+ "93": [0.25, 0.75, 0.10528, 0],
+ "94": [0, 0.69444, 0.06646, 0],
+ "95": [0.31, 0.12056, 0.09208, 0],
+ "97": [0, 0.43056, 0.07671, 0],
+ "98": [0, 0.69444, 0.06312, 0],
+ "99": [0, 0.43056, 0.05653, 0],
+ "100": [0, 0.69444, 0.10333, 0],
+ "101": [0, 0.43056, 0.07514, 0],
+ "102": [0.19444, 0.69444, 0.21194, 0],
+ "103": [0.19444, 0.43056, 0.08847, 0],
+ "104": [0, 0.69444, 0.07671, 0],
+ "105": [0, 0.65536, 0.1019, 0],
+ "106": [0.19444, 0.65536, 0.14467, 0],
+ "107": [0, 0.69444, 0.10764, 0],
+ "108": [0, 0.69444, 0.10333, 0],
+ "109": [0, 0.43056, 0.07671, 0],
+ "110": [0, 0.43056, 0.07671, 0],
+ "111": [0, 0.43056, 0.06312, 0],
+ "112": [0.19444, 0.43056, 0.06312, 0],
+ "113": [0.19444, 0.43056, 0.08847, 0],
+ "114": [0, 0.43056, 0.10764, 0],
+ "115": [0, 0.43056, 0.08208, 0],
+ "116": [0, 0.61508, 0.09486, 0],
+ "117": [0, 0.43056, 0.07671, 0],
+ "118": [0, 0.43056, 0.10764, 0],
+ "119": [0, 0.43056, 0.10764, 0],
+ "120": [0, 0.43056, 0.12042, 0],
+ "121": [0.19444, 0.43056, 0.08847, 0],
+ "122": [0, 0.43056, 0.12292, 0],
+ "126": [0.35, 0.31786, 0.11585, 0],
+ "163": [0, 0.69444, 0, 0],
+ "305": [0, 0.43056, 0, 0.02778],
+ "567": [0.19444, 0.43056, 0, 0.08334],
+ "768": [0, 0.69444, 0, 0],
+ "769": [0, 0.69444, 0.09694, 0],
+ "770": [0, 0.69444, 0.06646, 0],
+ "771": [0, 0.66786, 0.11585, 0],
+ "772": [0, 0.56167, 0.10333, 0],
+ "774": [0, 0.69444, 0.10806, 0],
+ "775": [0, 0.66786, 0.11752, 0],
+ "776": [0, 0.66786, 0.10474, 0],
+ "778": [0, 0.69444, 0, 0],
+ "779": [0, 0.69444, 0.1225, 0],
+ "780": [0, 0.62847, 0.08295, 0],
+ "915": [0, 0.68333, 0.13305, 0],
+ "916": [0, 0.68333, 0, 0],
+ "920": [0, 0.68333, 0.09403, 0],
+ "923": [0, 0.68333, 0, 0],
+ "926": [0, 0.68333, 0.15294, 0],
+ "928": [0, 0.68333, 0.16389, 0],
+ "931": [0, 0.68333, 0.12028, 0],
+ "933": [0, 0.68333, 0.11111, 0],
+ "934": [0, 0.68333, 0.05986, 0],
+ "936": [0, 0.68333, 0.11111, 0],
+ "937": [0, 0.68333, 0.10257, 0],
+ "8211": [0, 0.43056, 0.09208, 0],
+ "8212": [0, 0.43056, 0.09208, 0],
+ "8216": [0, 0.69444, 0.12417, 0],
+ "8217": [0, 0.69444, 0.12417, 0],
+ "8220": [0, 0.69444, 0.1685, 0],
+ "8221": [0, 0.69444, 0.06961, 0],
+ "8463": [0, 0.68889, 0, 0],
+ },
+ "Main-Regular": {
+ "32": [0, 0, 0, 0],
+ "33": [0, 0.69444, 0, 0],
+ "34": [0, 0.69444, 0, 0],
+ "35": [0.19444, 0.69444, 0, 0],
+ "36": [0.05556, 0.75, 0, 0],
+ "37": [0.05556, 0.75, 0, 0],
+ "38": [0, 0.69444, 0, 0],
+ "39": [0, 0.69444, 0, 0],
+ "40": [0.25, 0.75, 0, 0],
+ "41": [0.25, 0.75, 0, 0],
+ "42": [0, 0.75, 0, 0],
+ "43": [0.08333, 0.58333, 0, 0],
+ "44": [0.19444, 0.10556, 0, 0],
+ "45": [0, 0.43056, 0, 0],
+ "46": [0, 0.10556, 0, 0],
+ "47": [0.25, 0.75, 0, 0],
+ "48": [0, 0.64444, 0, 0],
+ "49": [0, 0.64444, 0, 0],
+ "50": [0, 0.64444, 0, 0],
+ "51": [0, 0.64444, 0, 0],
+ "52": [0, 0.64444, 0, 0],
+ "53": [0, 0.64444, 0, 0],
+ "54": [0, 0.64444, 0, 0],
+ "55": [0, 0.64444, 0, 0],
+ "56": [0, 0.64444, 0, 0],
+ "57": [0, 0.64444, 0, 0],
+ "58": [0, 0.43056, 0, 0],
+ "59": [0.19444, 0.43056, 0, 0],
+ "60": [0.0391, 0.5391, 0, 0],
+ "61": [-0.13313, 0.36687, 0, 0],
+ "62": [0.0391, 0.5391, 0, 0],
+ "63": [0, 0.69444, 0, 0],
+ "64": [0, 0.69444, 0, 0],
+ "65": [0, 0.68333, 0, 0],
+ "66": [0, 0.68333, 0, 0],
+ "67": [0, 0.68333, 0, 0],
+ "68": [0, 0.68333, 0, 0],
+ "69": [0, 0.68333, 0, 0],
+ "70": [0, 0.68333, 0, 0],
+ "71": [0, 0.68333, 0, 0],
+ "72": [0, 0.68333, 0, 0],
+ "73": [0, 0.68333, 0, 0],
+ "74": [0, 0.68333, 0, 0],
+ "75": [0, 0.68333, 0, 0],
+ "76": [0, 0.68333, 0, 0],
+ "77": [0, 0.68333, 0, 0],
+ "78": [0, 0.68333, 0, 0],
+ "79": [0, 0.68333, 0, 0],
+ "80": [0, 0.68333, 0, 0],
+ "81": [0.19444, 0.68333, 0, 0],
+ "82": [0, 0.68333, 0, 0],
+ "83": [0, 0.68333, 0, 0],
+ "84": [0, 0.68333, 0, 0],
+ "85": [0, 0.68333, 0, 0],
+ "86": [0, 0.68333, 0.01389, 0],
+ "87": [0, 0.68333, 0.01389, 0],
+ "88": [0, 0.68333, 0, 0],
+ "89": [0, 0.68333, 0.025, 0],
+ "90": [0, 0.68333, 0, 0],
+ "91": [0.25, 0.75, 0, 0],
+ "92": [0.25, 0.75, 0, 0],
+ "93": [0.25, 0.75, 0, 0],
+ "94": [0, 0.69444, 0, 0],
+ "95": [0.31, 0.12056, 0.02778, 0],
+ "96": [0, 0.69444, 0, 0],
+ "97": [0, 0.43056, 0, 0],
+ "98": [0, 0.69444, 0, 0],
+ "99": [0, 0.43056, 0, 0],
+ "100": [0, 0.69444, 0, 0],
+ "101": [0, 0.43056, 0, 0],
+ "102": [0, 0.69444, 0.07778, 0],
+ "103": [0.19444, 0.43056, 0.01389, 0],
+ "104": [0, 0.69444, 0, 0],
+ "105": [0, 0.66786, 0, 0],
+ "106": [0.19444, 0.66786, 0, 0],
+ "107": [0, 0.69444, 0, 0],
+ "108": [0, 0.69444, 0, 0],
+ "109": [0, 0.43056, 0, 0],
+ "110": [0, 0.43056, 0, 0],
+ "111": [0, 0.43056, 0, 0],
+ "112": [0.19444, 0.43056, 0, 0],
+ "113": [0.19444, 0.43056, 0, 0],
+ "114": [0, 0.43056, 0, 0],
+ "115": [0, 0.43056, 0, 0],
+ "116": [0, 0.61508, 0, 0],
+ "117": [0, 0.43056, 0, 0],
+ "118": [0, 0.43056, 0.01389, 0],
+ "119": [0, 0.43056, 0.01389, 0],
+ "120": [0, 0.43056, 0, 0],
+ "121": [0.19444, 0.43056, 0.01389, 0],
+ "122": [0, 0.43056, 0, 0],
+ "123": [0.25, 0.75, 0, 0],
+ "124": [0.25, 0.75, 0, 0],
+ "125": [0.25, 0.75, 0, 0],
+ "126": [0.35, 0.31786, 0, 0],
+ "160": [0, 0, 0, 0],
+ "168": [0, 0.66786, 0, 0],
+ "172": [0, 0.43056, 0, 0],
+ "175": [0, 0.56778, 0, 0],
+ "176": [0, 0.69444, 0, 0],
+ "177": [0.08333, 0.58333, 0, 0],
+ "180": [0, 0.69444, 0, 0],
+ "215": [0.08333, 0.58333, 0, 0],
+ "247": [0.08333, 0.58333, 0, 0],
+ "305": [0, 0.43056, 0, 0],
+ "567": [0.19444, 0.43056, 0, 0],
+ "710": [0, 0.69444, 0, 0],
+ "711": [0, 0.62847, 0, 0],
+ "713": [0, 0.56778, 0, 0],
+ "714": [0, 0.69444, 0, 0],
+ "715": [0, 0.69444, 0, 0],
+ "728": [0, 0.69444, 0, 0],
+ "729": [0, 0.66786, 0, 0],
+ "730": [0, 0.69444, 0, 0],
+ "732": [0, 0.66786, 0, 0],
+ "768": [0, 0.69444, 0, 0],
+ "769": [0, 0.69444, 0, 0],
+ "770": [0, 0.69444, 0, 0],
+ "771": [0, 0.66786, 0, 0],
+ "772": [0, 0.56778, 0, 0],
+ "774": [0, 0.69444, 0, 0],
+ "775": [0, 0.66786, 0, 0],
+ "776": [0, 0.66786, 0, 0],
+ "778": [0, 0.69444, 0, 0],
+ "779": [0, 0.69444, 0, 0],
+ "780": [0, 0.62847, 0, 0],
+ "824": [0.19444, 0.69444, 0, 0],
+ "915": [0, 0.68333, 0, 0],
+ "916": [0, 0.68333, 0, 0],
+ "920": [0, 0.68333, 0, 0],
+ "923": [0, 0.68333, 0, 0],
+ "926": [0, 0.68333, 0, 0],
+ "928": [0, 0.68333, 0, 0],
+ "931": [0, 0.68333, 0, 0],
+ "933": [0, 0.68333, 0, 0],
+ "934": [0, 0.68333, 0, 0],
+ "936": [0, 0.68333, 0, 0],
+ "937": [0, 0.68333, 0, 0],
+ "8211": [0, 0.43056, 0.02778, 0],
+ "8212": [0, 0.43056, 0.02778, 0],
+ "8216": [0, 0.69444, 0, 0],
+ "8217": [0, 0.69444, 0, 0],
+ "8220": [0, 0.69444, 0, 0],
+ "8221": [0, 0.69444, 0, 0],
+ "8224": [0.19444, 0.69444, 0, 0],
+ "8225": [0.19444, 0.69444, 0, 0],
+ "8230": [0, 0.12, 0, 0],
+ "8242": [0, 0.55556, 0, 0],
+ "8407": [0, 0.71444, 0.15382, 0],
+ "8463": [0, 0.68889, 0, 0],
+ "8465": [0, 0.69444, 0, 0],
+ "8467": [0, 0.69444, 0, 0.11111],
+ "8472": [0.19444, 0.43056, 0, 0.11111],
+ "8476": [0, 0.69444, 0, 0],
+ "8501": [0, 0.69444, 0, 0],
+ "8592": [-0.13313, 0.36687, 0, 0],
+ "8593": [0.19444, 0.69444, 0, 0],
+ "8594": [-0.13313, 0.36687, 0, 0],
+ "8595": [0.19444, 0.69444, 0, 0],
+ "8596": [-0.13313, 0.36687, 0, 0],
+ "8597": [0.25, 0.75, 0, 0],
+ "8598": [0.19444, 0.69444, 0, 0],
+ "8599": [0.19444, 0.69444, 0, 0],
+ "8600": [0.19444, 0.69444, 0, 0],
+ "8601": [0.19444, 0.69444, 0, 0],
+ "8614": [0.011, 0.511, 0, 0],
+ "8617": [0.011, 0.511, 0, 0],
+ "8618": [0.011, 0.511, 0, 0],
+ "8636": [-0.13313, 0.36687, 0, 0],
+ "8637": [-0.13313, 0.36687, 0, 0],
+ "8640": [-0.13313, 0.36687, 0, 0],
+ "8641": [-0.13313, 0.36687, 0, 0],
+ "8652": [0.011, 0.671, 0, 0],
+ "8656": [-0.13313, 0.36687, 0, 0],
+ "8657": [0.19444, 0.69444, 0, 0],
+ "8658": [-0.13313, 0.36687, 0, 0],
+ "8659": [0.19444, 0.69444, 0, 0],
+ "8660": [-0.13313, 0.36687, 0, 0],
+ "8661": [0.25, 0.75, 0, 0],
+ "8704": [0, 0.69444, 0, 0],
+ "8706": [0, 0.69444, 0.05556, 0.08334],
+ "8707": [0, 0.69444, 0, 0],
+ "8709": [0.05556, 0.75, 0, 0],
+ "8711": [0, 0.68333, 0, 0],
+ "8712": [0.0391, 0.5391, 0, 0],
+ "8715": [0.0391, 0.5391, 0, 0],
+ "8722": [0.08333, 0.58333, 0, 0],
+ "8723": [0.08333, 0.58333, 0, 0],
+ "8725": [0.25, 0.75, 0, 0],
+ "8726": [0.25, 0.75, 0, 0],
+ "8727": [-0.03472, 0.46528, 0, 0],
+ "8728": [-0.05555, 0.44445, 0, 0],
+ "8729": [-0.05555, 0.44445, 0, 0],
+ "8730": [0.2, 0.8, 0, 0],
+ "8733": [0, 0.43056, 0, 0],
+ "8734": [0, 0.43056, 0, 0],
+ "8736": [0, 0.69224, 0, 0],
+ "8739": [0.25, 0.75, 0, 0],
+ "8741": [0.25, 0.75, 0, 0],
+ "8743": [0, 0.55556, 0, 0],
+ "8744": [0, 0.55556, 0, 0],
+ "8745": [0, 0.55556, 0, 0],
+ "8746": [0, 0.55556, 0, 0],
+ "8747": [0.19444, 0.69444, 0.11111, 0],
+ "8764": [-0.13313, 0.36687, 0, 0],
+ "8768": [0.19444, 0.69444, 0, 0],
+ "8771": [-0.03625, 0.46375, 0, 0],
+ "8773": [-0.022, 0.589, 0, 0],
+ "8776": [-0.01688, 0.48312, 0, 0],
+ "8781": [-0.03625, 0.46375, 0, 0],
+ "8784": [-0.133, 0.67, 0, 0],
+ "8800": [0.215, 0.716, 0, 0],
+ "8801": [-0.03625, 0.46375, 0, 0],
+ "8804": [0.13597, 0.63597, 0, 0],
+ "8805": [0.13597, 0.63597, 0, 0],
+ "8810": [0.0391, 0.5391, 0, 0],
+ "8811": [0.0391, 0.5391, 0, 0],
+ "8826": [0.0391, 0.5391, 0, 0],
+ "8827": [0.0391, 0.5391, 0, 0],
+ "8834": [0.0391, 0.5391, 0, 0],
+ "8835": [0.0391, 0.5391, 0, 0],
+ "8838": [0.13597, 0.63597, 0, 0],
+ "8839": [0.13597, 0.63597, 0, 0],
+ "8846": [0, 0.55556, 0, 0],
+ "8849": [0.13597, 0.63597, 0, 0],
+ "8850": [0.13597, 0.63597, 0, 0],
+ "8851": [0, 0.55556, 0, 0],
+ "8852": [0, 0.55556, 0, 0],
+ "8853": [0.08333, 0.58333, 0, 0],
+ "8854": [0.08333, 0.58333, 0, 0],
+ "8855": [0.08333, 0.58333, 0, 0],
+ "8856": [0.08333, 0.58333, 0, 0],
+ "8857": [0.08333, 0.58333, 0, 0],
+ "8866": [0, 0.69444, 0, 0],
+ "8867": [0, 0.69444, 0, 0],
+ "8868": [0, 0.69444, 0, 0],
+ "8869": [0, 0.69444, 0, 0],
+ "8872": [0.249, 0.75, 0, 0],
+ "8900": [-0.05555, 0.44445, 0, 0],
+ "8901": [-0.05555, 0.44445, 0, 0],
+ "8902": [-0.03472, 0.46528, 0, 0],
+ "8904": [0.005, 0.505, 0, 0],
+ "8942": [0.03, 0.9, 0, 0],
+ "8943": [-0.19, 0.31, 0, 0],
+ "8945": [-0.1, 0.82, 0, 0],
+ "8968": [0.25, 0.75, 0, 0],
+ "8969": [0.25, 0.75, 0, 0],
+ "8970": [0.25, 0.75, 0, 0],
+ "8971": [0.25, 0.75, 0, 0],
+ "8994": [-0.14236, 0.35764, 0, 0],
+ "8995": [-0.14236, 0.35764, 0, 0],
+ "9136": [0.244, 0.744, 0, 0],
+ "9137": [0.244, 0.744, 0, 0],
+ "9651": [0.19444, 0.69444, 0, 0],
+ "9657": [-0.03472, 0.46528, 0, 0],
+ "9661": [0.19444, 0.69444, 0, 0],
+ "9667": [-0.03472, 0.46528, 0, 0],
+ "9711": [0.19444, 0.69444, 0, 0],
+ "9824": [0.12963, 0.69444, 0, 0],
+ "9825": [0.12963, 0.69444, 0, 0],
+ "9826": [0.12963, 0.69444, 0, 0],
+ "9827": [0.12963, 0.69444, 0, 0],
+ "9837": [0, 0.75, 0, 0],
+ "9838": [0.19444, 0.69444, 0, 0],
+ "9839": [0.19444, 0.69444, 0, 0],
+ "10216": [0.25, 0.75, 0, 0],
+ "10217": [0.25, 0.75, 0, 0],
+ "10222": [0.244, 0.744, 0, 0],
+ "10223": [0.244, 0.744, 0, 0],
+ "10229": [0.011, 0.511, 0, 0],
+ "10230": [0.011, 0.511, 0, 0],
+ "10231": [0.011, 0.511, 0, 0],
+ "10232": [0.024, 0.525, 0, 0],
+ "10233": [0.024, 0.525, 0, 0],
+ "10234": [0.024, 0.525, 0, 0],
+ "10236": [0.011, 0.511, 0, 0],
+ "10815": [0, 0.68333, 0, 0],
+ "10927": [0.13597, 0.63597, 0, 0],
+ "10928": [0.13597, 0.63597, 0, 0],
+ },
+ "Math-BoldItalic": {
+ "47": [0.19444, 0.69444, 0, 0],
+ "65": [0, 0.68611, 0, 0],
+ "66": [0, 0.68611, 0.04835, 0],
+ "67": [0, 0.68611, 0.06979, 0],
+ "68": [0, 0.68611, 0.03194, 0],
+ "69": [0, 0.68611, 0.05451, 0],
+ "70": [0, 0.68611, 0.15972, 0],
+ "71": [0, 0.68611, 0, 0],
+ "72": [0, 0.68611, 0.08229, 0],
+ "73": [0, 0.68611, 0.07778, 0],
+ "74": [0, 0.68611, 0.10069, 0],
+ "75": [0, 0.68611, 0.06979, 0],
+ "76": [0, 0.68611, 0, 0],
+ "77": [0, 0.68611, 0.11424, 0],
+ "78": [0, 0.68611, 0.11424, 0],
+ "79": [0, 0.68611, 0.03194, 0],
+ "80": [0, 0.68611, 0.15972, 0],
+ "81": [0.19444, 0.68611, 0, 0],
+ "82": [0, 0.68611, 0.00421, 0],
+ "83": [0, 0.68611, 0.05382, 0],
+ "84": [0, 0.68611, 0.15972, 0],
+ "85": [0, 0.68611, 0.11424, 0],
+ "86": [0, 0.68611, 0.25555, 0],
+ "87": [0, 0.68611, 0.15972, 0],
+ "88": [0, 0.68611, 0.07778, 0],
+ "89": [0, 0.68611, 0.25555, 0],
+ "90": [0, 0.68611, 0.06979, 0],
+ "97": [0, 0.44444, 0, 0],
+ "98": [0, 0.69444, 0, 0],
+ "99": [0, 0.44444, 0, 0],
+ "100": [0, 0.69444, 0, 0],
+ "101": [0, 0.44444, 0, 0],
+ "102": [0.19444, 0.69444, 0.11042, 0],
+ "103": [0.19444, 0.44444, 0.03704, 0],
+ "104": [0, 0.69444, 0, 0],
+ "105": [0, 0.69326, 0, 0],
+ "106": [0.19444, 0.69326, 0.0622, 0],
+ "107": [0, 0.69444, 0.01852, 0],
+ "108": [0, 0.69444, 0.0088, 0],
+ "109": [0, 0.44444, 0, 0],
+ "110": [0, 0.44444, 0, 0],
+ "111": [0, 0.44444, 0, 0],
+ "112": [0.19444, 0.44444, 0, 0],
+ "113": [0.19444, 0.44444, 0.03704, 0],
+ "114": [0, 0.44444, 0.03194, 0],
+ "115": [0, 0.44444, 0, 0],
+ "116": [0, 0.63492, 0, 0],
+ "117": [0, 0.44444, 0, 0],
+ "118": [0, 0.44444, 0.03704, 0],
+ "119": [0, 0.44444, 0.02778, 0],
+ "120": [0, 0.44444, 0, 0],
+ "121": [0.19444, 0.44444, 0.03704, 0],
+ "122": [0, 0.44444, 0.04213, 0],
+ "915": [0, 0.68611, 0.15972, 0],
+ "916": [0, 0.68611, 0, 0],
+ "920": [0, 0.68611, 0.03194, 0],
+ "923": [0, 0.68611, 0, 0],
+ "926": [0, 0.68611, 0.07458, 0],
+ "928": [0, 0.68611, 0.08229, 0],
+ "931": [0, 0.68611, 0.05451, 0],
+ "933": [0, 0.68611, 0.15972, 0],
+ "934": [0, 0.68611, 0, 0],
+ "936": [0, 0.68611, 0.11653, 0],
+ "937": [0, 0.68611, 0.04835, 0],
+ "945": [0, 0.44444, 0, 0],
+ "946": [0.19444, 0.69444, 0.03403, 0],
+ "947": [0.19444, 0.44444, 0.06389, 0],
+ "948": [0, 0.69444, 0.03819, 0],
+ "949": [0, 0.44444, 0, 0],
+ "950": [0.19444, 0.69444, 0.06215, 0],
+ "951": [0.19444, 0.44444, 0.03704, 0],
+ "952": [0, 0.69444, 0.03194, 0],
+ "953": [0, 0.44444, 0, 0],
+ "954": [0, 0.44444, 0, 0],
+ "955": [0, 0.69444, 0, 0],
+ "956": [0.19444, 0.44444, 0, 0],
+ "957": [0, 0.44444, 0.06898, 0],
+ "958": [0.19444, 0.69444, 0.03021, 0],
+ "959": [0, 0.44444, 0, 0],
+ "960": [0, 0.44444, 0.03704, 0],
+ "961": [0.19444, 0.44444, 0, 0],
+ "962": [0.09722, 0.44444, 0.07917, 0],
+ "963": [0, 0.44444, 0.03704, 0],
+ "964": [0, 0.44444, 0.13472, 0],
+ "965": [0, 0.44444, 0.03704, 0],
+ "966": [0.19444, 0.44444, 0, 0],
+ "967": [0.19444, 0.44444, 0, 0],
+ "968": [0.19444, 0.69444, 0.03704, 0],
+ "969": [0, 0.44444, 0.03704, 0],
+ "977": [0, 0.69444, 0, 0],
+ "981": [0.19444, 0.69444, 0, 0],
+ "982": [0, 0.44444, 0.03194, 0],
+ "1009": [0.19444, 0.44444, 0, 0],
+ "1013": [0, 0.44444, 0, 0],
+ },
+ "Math-Italic": {
+ "47": [0.19444, 0.69444, 0, 0],
+ "65": [0, 0.68333, 0, 0.13889],
+ "66": [0, 0.68333, 0.05017, 0.08334],
+ "67": [0, 0.68333, 0.07153, 0.08334],
+ "68": [0, 0.68333, 0.02778, 0.05556],
+ "69": [0, 0.68333, 0.05764, 0.08334],
+ "70": [0, 0.68333, 0.13889, 0.08334],
+ "71": [0, 0.68333, 0, 0.08334],
+ "72": [0, 0.68333, 0.08125, 0.05556],
+ "73": [0, 0.68333, 0.07847, 0.11111],
+ "74": [0, 0.68333, 0.09618, 0.16667],
+ "75": [0, 0.68333, 0.07153, 0.05556],
+ "76": [0, 0.68333, 0, 0.02778],
+ "77": [0, 0.68333, 0.10903, 0.08334],
+ "78": [0, 0.68333, 0.10903, 0.08334],
+ "79": [0, 0.68333, 0.02778, 0.08334],
+ "80": [0, 0.68333, 0.13889, 0.08334],
+ "81": [0.19444, 0.68333, 0, 0.08334],
+ "82": [0, 0.68333, 0.00773, 0.08334],
+ "83": [0, 0.68333, 0.05764, 0.08334],
+ "84": [0, 0.68333, 0.13889, 0.08334],
+ "85": [0, 0.68333, 0.10903, 0.02778],
+ "86": [0, 0.68333, 0.22222, 0],
+ "87": [0, 0.68333, 0.13889, 0],
+ "88": [0, 0.68333, 0.07847, 0.08334],
+ "89": [0, 0.68333, 0.22222, 0],
+ "90": [0, 0.68333, 0.07153, 0.08334],
+ "97": [0, 0.43056, 0, 0],
+ "98": [0, 0.69444, 0, 0],
+ "99": [0, 0.43056, 0, 0.05556],
+ "100": [0, 0.69444, 0, 0.16667],
+ "101": [0, 0.43056, 0, 0.05556],
+ "102": [0.19444, 0.69444, 0.10764, 0.16667],
+ "103": [0.19444, 0.43056, 0.03588, 0.02778],
+ "104": [0, 0.69444, 0, 0],
+ "105": [0, 0.65952, 0, 0],
+ "106": [0.19444, 0.65952, 0.05724, 0],
+ "107": [0, 0.69444, 0.03148, 0],
+ "108": [0, 0.69444, 0.01968, 0.08334],
+ "109": [0, 0.43056, 0, 0],
+ "110": [0, 0.43056, 0, 0],
+ "111": [0, 0.43056, 0, 0.05556],
+ "112": [0.19444, 0.43056, 0, 0.08334],
+ "113": [0.19444, 0.43056, 0.03588, 0.08334],
+ "114": [0, 0.43056, 0.02778, 0.05556],
+ "115": [0, 0.43056, 0, 0.05556],
+ "116": [0, 0.61508, 0, 0.08334],
+ "117": [0, 0.43056, 0, 0.02778],
+ "118": [0, 0.43056, 0.03588, 0.02778],
+ "119": [0, 0.43056, 0.02691, 0.08334],
+ "120": [0, 0.43056, 0, 0.02778],
+ "121": [0.19444, 0.43056, 0.03588, 0.05556],
+ "122": [0, 0.43056, 0.04398, 0.05556],
+ "915": [0, 0.68333, 0.13889, 0.08334],
+ "916": [0, 0.68333, 0, 0.16667],
+ "920": [0, 0.68333, 0.02778, 0.08334],
+ "923": [0, 0.68333, 0, 0.16667],
+ "926": [0, 0.68333, 0.07569, 0.08334],
+ "928": [0, 0.68333, 0.08125, 0.05556],
+ "931": [0, 0.68333, 0.05764, 0.08334],
+ "933": [0, 0.68333, 0.13889, 0.05556],
+ "934": [0, 0.68333, 0, 0.08334],
+ "936": [0, 0.68333, 0.11, 0.05556],
+ "937": [0, 0.68333, 0.05017, 0.08334],
+ "945": [0, 0.43056, 0.0037, 0.02778],
+ "946": [0.19444, 0.69444, 0.05278, 0.08334],
+ "947": [0.19444, 0.43056, 0.05556, 0],
+ "948": [0, 0.69444, 0.03785, 0.05556],
+ "949": [0, 0.43056, 0, 0.08334],
+ "950": [0.19444, 0.69444, 0.07378, 0.08334],
+ "951": [0.19444, 0.43056, 0.03588, 0.05556],
+ "952": [0, 0.69444, 0.02778, 0.08334],
+ "953": [0, 0.43056, 0, 0.05556],
+ "954": [0, 0.43056, 0, 0],
+ "955": [0, 0.69444, 0, 0],
+ "956": [0.19444, 0.43056, 0, 0.02778],
+ "957": [0, 0.43056, 0.06366, 0.02778],
+ "958": [0.19444, 0.69444, 0.04601, 0.11111],
+ "959": [0, 0.43056, 0, 0.05556],
+ "960": [0, 0.43056, 0.03588, 0],
+ "961": [0.19444, 0.43056, 0, 0.08334],
+ "962": [0.09722, 0.43056, 0.07986, 0.08334],
+ "963": [0, 0.43056, 0.03588, 0],
+ "964": [0, 0.43056, 0.1132, 0.02778],
+ "965": [0, 0.43056, 0.03588, 0.02778],
+ "966": [0.19444, 0.43056, 0, 0.08334],
+ "967": [0.19444, 0.43056, 0, 0.05556],
+ "968": [0.19444, 0.69444, 0.03588, 0.11111],
+ "969": [0, 0.43056, 0.03588, 0],
+ "977": [0, 0.69444, 0, 0.08334],
+ "981": [0.19444, 0.69444, 0, 0.08334],
+ "982": [0, 0.43056, 0.02778, 0],
+ "1009": [0.19444, 0.43056, 0, 0.08334],
+ "1013": [0, 0.43056, 0, 0.05556],
+ },
+ "Math-Regular": {
+ "65": [0, 0.68333, 0, 0.13889],
+ "66": [0, 0.68333, 0.05017, 0.08334],
+ "67": [0, 0.68333, 0.07153, 0.08334],
+ "68": [0, 0.68333, 0.02778, 0.05556],
+ "69": [0, 0.68333, 0.05764, 0.08334],
+ "70": [0, 0.68333, 0.13889, 0.08334],
+ "71": [0, 0.68333, 0, 0.08334],
+ "72": [0, 0.68333, 0.08125, 0.05556],
+ "73": [0, 0.68333, 0.07847, 0.11111],
+ "74": [0, 0.68333, 0.09618, 0.16667],
+ "75": [0, 0.68333, 0.07153, 0.05556],
+ "76": [0, 0.68333, 0, 0.02778],
+ "77": [0, 0.68333, 0.10903, 0.08334],
+ "78": [0, 0.68333, 0.10903, 0.08334],
+ "79": [0, 0.68333, 0.02778, 0.08334],
+ "80": [0, 0.68333, 0.13889, 0.08334],
+ "81": [0.19444, 0.68333, 0, 0.08334],
+ "82": [0, 0.68333, 0.00773, 0.08334],
+ "83": [0, 0.68333, 0.05764, 0.08334],
+ "84": [0, 0.68333, 0.13889, 0.08334],
+ "85": [0, 0.68333, 0.10903, 0.02778],
+ "86": [0, 0.68333, 0.22222, 0],
+ "87": [0, 0.68333, 0.13889, 0],
+ "88": [0, 0.68333, 0.07847, 0.08334],
+ "89": [0, 0.68333, 0.22222, 0],
+ "90": [0, 0.68333, 0.07153, 0.08334],
+ "97": [0, 0.43056, 0, 0],
+ "98": [0, 0.69444, 0, 0],
+ "99": [0, 0.43056, 0, 0.05556],
+ "100": [0, 0.69444, 0, 0.16667],
+ "101": [0, 0.43056, 0, 0.05556],
+ "102": [0.19444, 0.69444, 0.10764, 0.16667],
+ "103": [0.19444, 0.43056, 0.03588, 0.02778],
+ "104": [0, 0.69444, 0, 0],
+ "105": [0, 0.65952, 0, 0],
+ "106": [0.19444, 0.65952, 0.05724, 0],
+ "107": [0, 0.69444, 0.03148, 0],
+ "108": [0, 0.69444, 0.01968, 0.08334],
+ "109": [0, 0.43056, 0, 0],
+ "110": [0, 0.43056, 0, 0],
+ "111": [0, 0.43056, 0, 0.05556],
+ "112": [0.19444, 0.43056, 0, 0.08334],
+ "113": [0.19444, 0.43056, 0.03588, 0.08334],
+ "114": [0, 0.43056, 0.02778, 0.05556],
+ "115": [0, 0.43056, 0, 0.05556],
+ "116": [0, 0.61508, 0, 0.08334],
+ "117": [0, 0.43056, 0, 0.02778],
+ "118": [0, 0.43056, 0.03588, 0.02778],
+ "119": [0, 0.43056, 0.02691, 0.08334],
+ "120": [0, 0.43056, 0, 0.02778],
+ "121": [0.19444, 0.43056, 0.03588, 0.05556],
+ "122": [0, 0.43056, 0.04398, 0.05556],
+ "915": [0, 0.68333, 0.13889, 0.08334],
+ "916": [0, 0.68333, 0, 0.16667],
+ "920": [0, 0.68333, 0.02778, 0.08334],
+ "923": [0, 0.68333, 0, 0.16667],
+ "926": [0, 0.68333, 0.07569, 0.08334],
+ "928": [0, 0.68333, 0.08125, 0.05556],
+ "931": [0, 0.68333, 0.05764, 0.08334],
+ "933": [0, 0.68333, 0.13889, 0.05556],
+ "934": [0, 0.68333, 0, 0.08334],
+ "936": [0, 0.68333, 0.11, 0.05556],
+ "937": [0, 0.68333, 0.05017, 0.08334],
+ "945": [0, 0.43056, 0.0037, 0.02778],
+ "946": [0.19444, 0.69444, 0.05278, 0.08334],
+ "947": [0.19444, 0.43056, 0.05556, 0],
+ "948": [0, 0.69444, 0.03785, 0.05556],
+ "949": [0, 0.43056, 0, 0.08334],
+ "950": [0.19444, 0.69444, 0.07378, 0.08334],
+ "951": [0.19444, 0.43056, 0.03588, 0.05556],
+ "952": [0, 0.69444, 0.02778, 0.08334],
+ "953": [0, 0.43056, 0, 0.05556],
+ "954": [0, 0.43056, 0, 0],
+ "955": [0, 0.69444, 0, 0],
+ "956": [0.19444, 0.43056, 0, 0.02778],
+ "957": [0, 0.43056, 0.06366, 0.02778],
+ "958": [0.19444, 0.69444, 0.04601, 0.11111],
+ "959": [0, 0.43056, 0, 0.05556],
+ "960": [0, 0.43056, 0.03588, 0],
+ "961": [0.19444, 0.43056, 0, 0.08334],
+ "962": [0.09722, 0.43056, 0.07986, 0.08334],
+ "963": [0, 0.43056, 0.03588, 0],
+ "964": [0, 0.43056, 0.1132, 0.02778],
+ "965": [0, 0.43056, 0.03588, 0.02778],
+ "966": [0.19444, 0.43056, 0, 0.08334],
+ "967": [0.19444, 0.43056, 0, 0.05556],
+ "968": [0.19444, 0.69444, 0.03588, 0.11111],
+ "969": [0, 0.43056, 0.03588, 0],
+ "977": [0, 0.69444, 0, 0.08334],
+ "981": [0.19444, 0.69444, 0, 0.08334],
+ "982": [0, 0.43056, 0.02778, 0],
+ "1009": [0.19444, 0.43056, 0, 0.08334],
+ "1013": [0, 0.43056, 0, 0.05556],
+ },
+ "SansSerif-Regular": {
+ "33": [0, 0.69444, 0, 0],
+ "34": [0, 0.69444, 0, 0],
+ "35": [0.19444, 0.69444, 0, 0],
+ "36": [0.05556, 0.75, 0, 0],
+ "37": [0.05556, 0.75, 0, 0],
+ "38": [0, 0.69444, 0, 0],
+ "39": [0, 0.69444, 0, 0],
+ "40": [0.25, 0.75, 0, 0],
+ "41": [0.25, 0.75, 0, 0],
+ "42": [0, 0.75, 0, 0],
+ "43": [0.08333, 0.58333, 0, 0],
+ "44": [0.125, 0.08333, 0, 0],
+ "45": [0, 0.44444, 0, 0],
+ "46": [0, 0.08333, 0, 0],
+ "47": [0.25, 0.75, 0, 0],
+ "48": [0, 0.65556, 0, 0],
+ "49": [0, 0.65556, 0, 0],
+ "50": [0, 0.65556, 0, 0],
+ "51": [0, 0.65556, 0, 0],
+ "52": [0, 0.65556, 0, 0],
+ "53": [0, 0.65556, 0, 0],
+ "54": [0, 0.65556, 0, 0],
+ "55": [0, 0.65556, 0, 0],
+ "56": [0, 0.65556, 0, 0],
+ "57": [0, 0.65556, 0, 0],
+ "58": [0, 0.44444, 0, 0],
+ "59": [0.125, 0.44444, 0, 0],
+ "61": [-0.13, 0.37, 0, 0],
+ "63": [0, 0.69444, 0, 0],
+ "64": [0, 0.69444, 0, 0],
+ "65": [0, 0.69444, 0, 0],
+ "66": [0, 0.69444, 0, 0],
+ "67": [0, 0.69444, 0, 0],
+ "68": [0, 0.69444, 0, 0],
+ "69": [0, 0.69444, 0, 0],
+ "70": [0, 0.69444, 0, 0],
+ "71": [0, 0.69444, 0, 0],
+ "72": [0, 0.69444, 0, 0],
+ "73": [0, 0.69444, 0, 0],
+ "74": [0, 0.69444, 0, 0],
+ "75": [0, 0.69444, 0, 0],
+ "76": [0, 0.69444, 0, 0],
+ "77": [0, 0.69444, 0, 0],
+ "78": [0, 0.69444, 0, 0],
+ "79": [0, 0.69444, 0, 0],
+ "80": [0, 0.69444, 0, 0],
+ "81": [0.125, 0.69444, 0, 0],
+ "82": [0, 0.69444, 0, 0],
+ "83": [0, 0.69444, 0, 0],
+ "84": [0, 0.69444, 0, 0],
+ "85": [0, 0.69444, 0, 0],
+ "86": [0, 0.69444, 0.01389, 0],
+ "87": [0, 0.69444, 0.01389, 0],
+ "88": [0, 0.69444, 0, 0],
+ "89": [0, 0.69444, 0.025, 0],
+ "90": [0, 0.69444, 0, 0],
+ "91": [0.25, 0.75, 0, 0],
+ "93": [0.25, 0.75, 0, 0],
+ "94": [0, 0.69444, 0, 0],
+ "95": [0.35, 0.09444, 0.02778, 0],
+ "97": [0, 0.44444, 0, 0],
+ "98": [0, 0.69444, 0, 0],
+ "99": [0, 0.44444, 0, 0],
+ "100": [0, 0.69444, 0, 0],
+ "101": [0, 0.44444, 0, 0],
+ "102": [0, 0.69444, 0.06944, 0],
+ "103": [0.19444, 0.44444, 0.01389, 0],
+ "104": [0, 0.69444, 0, 0],
+ "105": [0, 0.67937, 0, 0],
+ "106": [0.19444, 0.67937, 0, 0],
+ "107": [0, 0.69444, 0, 0],
+ "108": [0, 0.69444, 0, 0],
+ "109": [0, 0.44444, 0, 0],
+ "110": [0, 0.44444, 0, 0],
+ "111": [0, 0.44444, 0, 0],
+ "112": [0.19444, 0.44444, 0, 0],
+ "113": [0.19444, 0.44444, 0, 0],
+ "114": [0, 0.44444, 0.01389, 0],
+ "115": [0, 0.44444, 0, 0],
+ "116": [0, 0.57143, 0, 0],
+ "117": [0, 0.44444, 0, 0],
+ "118": [0, 0.44444, 0.01389, 0],
+ "119": [0, 0.44444, 0.01389, 0],
+ "120": [0, 0.44444, 0, 0],
+ "121": [0.19444, 0.44444, 0.01389, 0],
+ "122": [0, 0.44444, 0, 0],
+ "126": [0.35, 0.32659, 0, 0],
+ "305": [0, 0.44444, 0, 0],
+ "567": [0.19444, 0.44444, 0, 0],
+ "768": [0, 0.69444, 0, 0],
+ "769": [0, 0.69444, 0, 0],
+ "770": [0, 0.69444, 0, 0],
+ "771": [0, 0.67659, 0, 0],
+ "772": [0, 0.60889, 0, 0],
+ "774": [0, 0.69444, 0, 0],
+ "775": [0, 0.67937, 0, 0],
+ "776": [0, 0.67937, 0, 0],
+ "778": [0, 0.69444, 0, 0],
+ "779": [0, 0.69444, 0, 0],
+ "780": [0, 0.63194, 0, 0],
+ "915": [0, 0.69444, 0, 0],
+ "916": [0, 0.69444, 0, 0],
+ "920": [0, 0.69444, 0, 0],
+ "923": [0, 0.69444, 0, 0],
+ "926": [0, 0.69444, 0, 0],
+ "928": [0, 0.69444, 0, 0],
+ "931": [0, 0.69444, 0, 0],
+ "933": [0, 0.69444, 0, 0],
+ "934": [0, 0.69444, 0, 0],
+ "936": [0, 0.69444, 0, 0],
+ "937": [0, 0.69444, 0, 0],
+ "8211": [0, 0.44444, 0.02778, 0],
+ "8212": [0, 0.44444, 0.02778, 0],
+ "8216": [0, 0.69444, 0, 0],
+ "8217": [0, 0.69444, 0, 0],
+ "8220": [0, 0.69444, 0, 0],
+ "8221": [0, 0.69444, 0, 0],
+ },
+ "Script-Regular": {
+ "65": [0, 0.7, 0.22925, 0],
+ "66": [0, 0.7, 0.04087, 0],
+ "67": [0, 0.7, 0.1689, 0],
+ "68": [0, 0.7, 0.09371, 0],
+ "69": [0, 0.7, 0.18583, 0],
+ "70": [0, 0.7, 0.13634, 0],
+ "71": [0, 0.7, 0.17322, 0],
+ "72": [0, 0.7, 0.29694, 0],
+ "73": [0, 0.7, 0.19189, 0],
+ "74": [0.27778, 0.7, 0.19189, 0],
+ "75": [0, 0.7, 0.31259, 0],
+ "76": [0, 0.7, 0.19189, 0],
+ "77": [0, 0.7, 0.15981, 0],
+ "78": [0, 0.7, 0.3525, 0],
+ "79": [0, 0.7, 0.08078, 0],
+ "80": [0, 0.7, 0.08078, 0],
+ "81": [0, 0.7, 0.03305, 0],
+ "82": [0, 0.7, 0.06259, 0],
+ "83": [0, 0.7, 0.19189, 0],
+ "84": [0, 0.7, 0.29087, 0],
+ "85": [0, 0.7, 0.25815, 0],
+ "86": [0, 0.7, 0.27523, 0],
+ "87": [0, 0.7, 0.27523, 0],
+ "88": [0, 0.7, 0.26006, 0],
+ "89": [0, 0.7, 0.2939, 0],
+ "90": [0, 0.7, 0.24037, 0],
+ },
+ "Size1-Regular": {
+ "40": [0.35001, 0.85, 0, 0],
+ "41": [0.35001, 0.85, 0, 0],
+ "47": [0.35001, 0.85, 0, 0],
+ "91": [0.35001, 0.85, 0, 0],
+ "92": [0.35001, 0.85, 0, 0],
+ "93": [0.35001, 0.85, 0, 0],
+ "123": [0.35001, 0.85, 0, 0],
+ "125": [0.35001, 0.85, 0, 0],
+ "710": [0, 0.72222, 0, 0],
+ "732": [0, 0.72222, 0, 0],
+ "770": [0, 0.72222, 0, 0],
+ "771": [0, 0.72222, 0, 0],
+ "8214": [-0.00099, 0.601, 0, 0],
+ "8593": [1e-05, 0.6, 0, 0],
+ "8595": [1e-05, 0.6, 0, 0],
+ "8657": [1e-05, 0.6, 0, 0],
+ "8659": [1e-05, 0.6, 0, 0],
+ "8719": [0.25001, 0.75, 0, 0],
+ "8720": [0.25001, 0.75, 0, 0],
+ "8721": [0.25001, 0.75, 0, 0],
+ "8730": [0.35001, 0.85, 0, 0],
+ "8739": [-0.00599, 0.606, 0, 0],
+ "8741": [-0.00599, 0.606, 0, 0],
+ "8747": [0.30612, 0.805, 0.19445, 0],
+ "8748": [0.306, 0.805, 0.19445, 0],
+ "8749": [0.306, 0.805, 0.19445, 0],
+ "8750": [0.30612, 0.805, 0.19445, 0],
+ "8896": [0.25001, 0.75, 0, 0],
+ "8897": [0.25001, 0.75, 0, 0],
+ "8898": [0.25001, 0.75, 0, 0],
+ "8899": [0.25001, 0.75, 0, 0],
+ "8968": [0.35001, 0.85, 0, 0],
+ "8969": [0.35001, 0.85, 0, 0],
+ "8970": [0.35001, 0.85, 0, 0],
+ "8971": [0.35001, 0.85, 0, 0],
+ "9168": [-0.00099, 0.601, 0, 0],
+ "10216": [0.35001, 0.85, 0, 0],
+ "10217": [0.35001, 0.85, 0, 0],
+ "10752": [0.25001, 0.75, 0, 0],
+ "10753": [0.25001, 0.75, 0, 0],
+ "10754": [0.25001, 0.75, 0, 0],
+ "10756": [0.25001, 0.75, 0, 0],
+ "10758": [0.25001, 0.75, 0, 0],
+ },
+ "Size2-Regular": {
+ "40": [0.65002, 1.15, 0, 0],
+ "41": [0.65002, 1.15, 0, 0],
+ "47": [0.65002, 1.15, 0, 0],
+ "91": [0.65002, 1.15, 0, 0],
+ "92": [0.65002, 1.15, 0, 0],
+ "93": [0.65002, 1.15, 0, 0],
+ "123": [0.65002, 1.15, 0, 0],
+ "125": [0.65002, 1.15, 0, 0],
+ "710": [0, 0.75, 0, 0],
+ "732": [0, 0.75, 0, 0],
+ "770": [0, 0.75, 0, 0],
+ "771": [0, 0.75, 0, 0],
+ "8719": [0.55001, 1.05, 0, 0],
+ "8720": [0.55001, 1.05, 0, 0],
+ "8721": [0.55001, 1.05, 0, 0],
+ "8730": [0.65002, 1.15, 0, 0],
+ "8747": [0.86225, 1.36, 0.44445, 0],
+ "8748": [0.862, 1.36, 0.44445, 0],
+ "8749": [0.862, 1.36, 0.44445, 0],
+ "8750": [0.86225, 1.36, 0.44445, 0],
+ "8896": [0.55001, 1.05, 0, 0],
+ "8897": [0.55001, 1.05, 0, 0],
+ "8898": [0.55001, 1.05, 0, 0],
+ "8899": [0.55001, 1.05, 0, 0],
+ "8968": [0.65002, 1.15, 0, 0],
+ "8969": [0.65002, 1.15, 0, 0],
+ "8970": [0.65002, 1.15, 0, 0],
+ "8971": [0.65002, 1.15, 0, 0],
+ "10216": [0.65002, 1.15, 0, 0],
+ "10217": [0.65002, 1.15, 0, 0],
+ "10752": [0.55001, 1.05, 0, 0],
+ "10753": [0.55001, 1.05, 0, 0],
+ "10754": [0.55001, 1.05, 0, 0],
+ "10756": [0.55001, 1.05, 0, 0],
+ "10758": [0.55001, 1.05, 0, 0],
+ },
+ "Size3-Regular": {
+ "40": [0.95003, 1.45, 0, 0],
+ "41": [0.95003, 1.45, 0, 0],
+ "47": [0.95003, 1.45, 0, 0],
+ "91": [0.95003, 1.45, 0, 0],
+ "92": [0.95003, 1.45, 0, 0],
+ "93": [0.95003, 1.45, 0, 0],
+ "123": [0.95003, 1.45, 0, 0],
+ "125": [0.95003, 1.45, 0, 0],
+ "710": [0, 0.75, 0, 0],
+ "732": [0, 0.75, 0, 0],
+ "770": [0, 0.75, 0, 0],
+ "771": [0, 0.75, 0, 0],
+ "8730": [0.95003, 1.45, 0, 0],
+ "8968": [0.95003, 1.45, 0, 0],
+ "8969": [0.95003, 1.45, 0, 0],
+ "8970": [0.95003, 1.45, 0, 0],
+ "8971": [0.95003, 1.45, 0, 0],
+ "10216": [0.95003, 1.45, 0, 0],
+ "10217": [0.95003, 1.45, 0, 0],
+ },
+ "Size4-Regular": {
+ "40": [1.25003, 1.75, 0, 0],
+ "41": [1.25003, 1.75, 0, 0],
+ "47": [1.25003, 1.75, 0, 0],
+ "91": [1.25003, 1.75, 0, 0],
+ "92": [1.25003, 1.75, 0, 0],
+ "93": [1.25003, 1.75, 0, 0],
+ "123": [1.25003, 1.75, 0, 0],
+ "125": [1.25003, 1.75, 0, 0],
+ "710": [0, 0.825, 0, 0],
+ "732": [0, 0.825, 0, 0],
+ "770": [0, 0.825, 0, 0],
+ "771": [0, 0.825, 0, 0],
+ "8730": [1.25003, 1.75, 0, 0],
+ "8968": [1.25003, 1.75, 0, 0],
+ "8969": [1.25003, 1.75, 0, 0],
+ "8970": [1.25003, 1.75, 0, 0],
+ "8971": [1.25003, 1.75, 0, 0],
+ "9115": [0.64502, 1.155, 0, 0],
+ "9116": [1e-05, 0.6, 0, 0],
+ "9117": [0.64502, 1.155, 0, 0],
+ "9118": [0.64502, 1.155, 0, 0],
+ "9119": [1e-05, 0.6, 0, 0],
+ "9120": [0.64502, 1.155, 0, 0],
+ "9121": [0.64502, 1.155, 0, 0],
+ "9122": [-0.00099, 0.601, 0, 0],
+ "9123": [0.64502, 1.155, 0, 0],
+ "9124": [0.64502, 1.155, 0, 0],
+ "9125": [-0.00099, 0.601, 0, 0],
+ "9126": [0.64502, 1.155, 0, 0],
+ "9127": [1e-05, 0.9, 0, 0],
+ "9128": [0.65002, 1.15, 0, 0],
+ "9129": [0.90001, 0, 0, 0],
+ "9130": [0, 0.3, 0, 0],
+ "9131": [1e-05, 0.9, 0, 0],
+ "9132": [0.65002, 1.15, 0, 0],
+ "9133": [0.90001, 0, 0, 0],
+ "9143": [0.88502, 0.915, 0, 0],
+ "10216": [1.25003, 1.75, 0, 0],
+ "10217": [1.25003, 1.75, 0, 0],
+ "57344": [-0.00499, 0.605, 0, 0],
+ "57345": [-0.00499, 0.605, 0, 0],
+ "57680": [0, 0.12, 0, 0],
+ "57681": [0, 0.12, 0, 0],
+ "57682": [0, 0.12, 0, 0],
+ "57683": [0, 0.12, 0, 0],
+ },
+ "Typewriter-Regular": {
+ "33": [0, 0.61111, 0, 0],
+ "34": [0, 0.61111, 0, 0],
+ "35": [0, 0.61111, 0, 0],
+ "36": [0.08333, 0.69444, 0, 0],
+ "37": [0.08333, 0.69444, 0, 0],
+ "38": [0, 0.61111, 0, 0],
+ "39": [0, 0.61111, 0, 0],
+ "40": [0.08333, 0.69444, 0, 0],
+ "41": [0.08333, 0.69444, 0, 0],
+ "42": [0, 0.52083, 0, 0],
+ "43": [-0.08056, 0.53055, 0, 0],
+ "44": [0.13889, 0.125, 0, 0],
+ "45": [-0.08056, 0.53055, 0, 0],
+ "46": [0, 0.125, 0, 0],
+ "47": [0.08333, 0.69444, 0, 0],
+ "48": [0, 0.61111, 0, 0],
+ "49": [0, 0.61111, 0, 0],
+ "50": [0, 0.61111, 0, 0],
+ "51": [0, 0.61111, 0, 0],
+ "52": [0, 0.61111, 0, 0],
+ "53": [0, 0.61111, 0, 0],
+ "54": [0, 0.61111, 0, 0],
+ "55": [0, 0.61111, 0, 0],
+ "56": [0, 0.61111, 0, 0],
+ "57": [0, 0.61111, 0, 0],
+ "58": [0, 0.43056, 0, 0],
+ "59": [0.13889, 0.43056, 0, 0],
+ "60": [-0.05556, 0.55556, 0, 0],
+ "61": [-0.19549, 0.41562, 0, 0],
+ "62": [-0.05556, 0.55556, 0, 0],
+ "63": [0, 0.61111, 0, 0],
+ "64": [0, 0.61111, 0, 0],
+ "65": [0, 0.61111, 0, 0],
+ "66": [0, 0.61111, 0, 0],
+ "67": [0, 0.61111, 0, 0],
+ "68": [0, 0.61111, 0, 0],
+ "69": [0, 0.61111, 0, 0],
+ "70": [0, 0.61111, 0, 0],
+ "71": [0, 0.61111, 0, 0],
+ "72": [0, 0.61111, 0, 0],
+ "73": [0, 0.61111, 0, 0],
+ "74": [0, 0.61111, 0, 0],
+ "75": [0, 0.61111, 0, 0],
+ "76": [0, 0.61111, 0, 0],
+ "77": [0, 0.61111, 0, 0],
+ "78": [0, 0.61111, 0, 0],
+ "79": [0, 0.61111, 0, 0],
+ "80": [0, 0.61111, 0, 0],
+ "81": [0.13889, 0.61111, 0, 0],
+ "82": [0, 0.61111, 0, 0],
+ "83": [0, 0.61111, 0, 0],
+ "84": [0, 0.61111, 0, 0],
+ "85": [0, 0.61111, 0, 0],
+ "86": [0, 0.61111, 0, 0],
+ "87": [0, 0.61111, 0, 0],
+ "88": [0, 0.61111, 0, 0],
+ "89": [0, 0.61111, 0, 0],
+ "90": [0, 0.61111, 0, 0],
+ "91": [0.08333, 0.69444, 0, 0],
+ "92": [0.08333, 0.69444, 0, 0],
+ "93": [0.08333, 0.69444, 0, 0],
+ "94": [0, 0.61111, 0, 0],
+ "95": [0.09514, 0, 0, 0],
+ "96": [0, 0.61111, 0, 0],
+ "97": [0, 0.43056, 0, 0],
+ "98": [0, 0.61111, 0, 0],
+ "99": [0, 0.43056, 0, 0],
+ "100": [0, 0.61111, 0, 0],
+ "101": [0, 0.43056, 0, 0],
+ "102": [0, 0.61111, 0, 0],
+ "103": [0.22222, 0.43056, 0, 0],
+ "104": [0, 0.61111, 0, 0],
+ "105": [0, 0.61111, 0, 0],
+ "106": [0.22222, 0.61111, 0, 0],
+ "107": [0, 0.61111, 0, 0],
+ "108": [0, 0.61111, 0, 0],
+ "109": [0, 0.43056, 0, 0],
+ "110": [0, 0.43056, 0, 0],
+ "111": [0, 0.43056, 0, 0],
+ "112": [0.22222, 0.43056, 0, 0],
+ "113": [0.22222, 0.43056, 0, 0],
+ "114": [0, 0.43056, 0, 0],
+ "115": [0, 0.43056, 0, 0],
+ "116": [0, 0.55358, 0, 0],
+ "117": [0, 0.43056, 0, 0],
+ "118": [0, 0.43056, 0, 0],
+ "119": [0, 0.43056, 0, 0],
+ "120": [0, 0.43056, 0, 0],
+ "121": [0.22222, 0.43056, 0, 0],
+ "122": [0, 0.43056, 0, 0],
+ "123": [0.08333, 0.69444, 0, 0],
+ "124": [0.08333, 0.69444, 0, 0],
+ "125": [0.08333, 0.69444, 0, 0],
+ "126": [0, 0.61111, 0, 0],
+ "127": [0, 0.61111, 0, 0],
+ "305": [0, 0.43056, 0, 0],
+ "567": [0.22222, 0.43056, 0, 0],
+ "768": [0, 0.61111, 0, 0],
+ "769": [0, 0.61111, 0, 0],
+ "770": [0, 0.61111, 0, 0],
+ "771": [0, 0.61111, 0, 0],
+ "772": [0, 0.56555, 0, 0],
+ "774": [0, 0.61111, 0, 0],
+ "776": [0, 0.61111, 0, 0],
+ "778": [0, 0.61111, 0, 0],
+ "780": [0, 0.56597, 0, 0],
+ "915": [0, 0.61111, 0, 0],
+ "916": [0, 0.61111, 0, 0],
+ "920": [0, 0.61111, 0, 0],
+ "923": [0, 0.61111, 0, 0],
+ "926": [0, 0.61111, 0, 0],
+ "928": [0, 0.61111, 0, 0],
+ "931": [0, 0.61111, 0, 0],
+ "933": [0, 0.61111, 0, 0],
+ "934": [0, 0.61111, 0, 0],
+ "936": [0, 0.61111, 0, 0],
+ "937": [0, 0.61111, 0, 0],
+ "2018": [0, 0.61111, 0, 0],
+ "2019": [0, 0.61111, 0, 0],
+ "8242": [0, 0.61111, 0, 0],
+ },
+};
+
+},{}],19:[function(require,module,exports){
+var utils = require("./utils");
+var ParseError = require("./ParseError");
+
+/* This file contains a list of functions that we parse, identified by
+ * the calls to defineFunction.
+ *
+ * The first argument to defineFunction is a single name or a list of names.
+ * All functions named in such a list will share a single implementation.
+ *
+ * Each declared function can have associated properties, which
+ * include the following:
+ *
+ * - numArgs: The number of arguments the function takes.
+ * If this is the only property, it can be passed as a number
+ * instead of an element of a properties object.
+ * - argTypes: (optional) An array corresponding to each argument of the
+ * function, giving the type of argument that should be parsed. Its
+ * length should be equal to `numArgs + numOptionalArgs`. Valid
+ * types:
+ * - "size": A size-like thing, such as "1em" or "5ex"
+ * - "color": An html color, like "#abc" or "blue"
+ * - "original": The same type as the environment that the
+ * function being parsed is in (e.g. used for the
+ * bodies of functions like \color where the first
+ * argument is special and the second argument is
+ * parsed normally)
+ * Other possible types (probably shouldn't be used)
+ * - "text": Text-like (e.g. \text)
+ * - "math": Normal math
+ * If undefined, this will be treated as an appropriate length
+ * array of "original" strings
+ * - greediness: (optional) The greediness of the function to use ungrouped
+ * arguments.
+ *
+ * E.g. if you have an expression
+ * \sqrt \frac 1 2
+ * since \frac has greediness=2 vs \sqrt's greediness=1, \frac
+ * will use the two arguments '1' and '2' as its two arguments,
+ * then that whole function will be used as the argument to
+ * \sqrt. On the other hand, the expressions
+ * \frac \frac 1 2 3
+ * and
+ * \frac \sqrt 1 2
+ * will fail because \frac and \frac have equal greediness
+ * and \sqrt has a lower greediness than \frac respectively. To
+ * make these parse, we would have to change them to:
+ * \frac {\frac 1 2} 3
+ * and
+ * \frac {\sqrt 1} 2
+ *
+ * The default value is `1`
+ * - allowedInText: (optional) Whether or not the function is allowed inside
+ * text mode (default false)
+ * - numOptionalArgs: (optional) The number of optional arguments the function
+ * should parse. If the optional arguments aren't found,
+ * `null` will be passed to the handler in their place.
+ * (default 0)
+ * - infix: (optional) Must be true if the function is an infix operator.
+ *
+ * The last argument is that implementation, the handler for the function(s).
+ * It is called to handle these functions and their arguments.
+ * It receives two arguments:
+ * - context contains information and references provided by the parser
+ * - args is an array of arguments obtained from TeX input
+ * The context contains the following properties:
+ * - funcName: the text (i.e. name) of the function, including \
+ * - parser: the parser object
+ * - lexer: the lexer object
+ * - positions: the positions in the overall string of the function
+ * and the arguments.
+ * The latter three should only be used to produce error messages.
+ *
+ * The function should return an object with the following keys:
+ * - type: The type of element that this is. This is then used in
+ * buildHTML/buildMathML to determine which function
+ * should be called to build this node into a DOM node
+ * Any other data can be added to the object, which will be passed
+ * in to the function in buildHTML/buildMathML as `group.value`.
+ */
+
+function defineFunction(names, props, handler) {
+ if (typeof names === "string") {
+ names = [names];
+ }
+ if (typeof props === "number") {
+ props = { numArgs: props };
+ }
+ // Set default values of functions
+ var data = {
+ numArgs: props.numArgs,
+ argTypes: props.argTypes,
+ greediness: (props.greediness === undefined) ? 1 : props.greediness,
+ allowedInText: !!props.allowedInText,
+ numOptionalArgs: props.numOptionalArgs || 0,
+ infix: !!props.infix,
+ handler: handler,
+ };
+ for (var i = 0; i < names.length; ++i) {
+ module.exports[names[i]] = data;
+ }
+}
+
+// A normal square root
+defineFunction("\\sqrt", {
+ numArgs: 1,
+ numOptionalArgs: 1,
+}, function(context, args) {
+ var index = args[0];
+ var body = args[1];
+ return {
+ type: "sqrt",
+ body: body,
+ index: index,
+ };
+});
+
+// Some non-mathy text
+defineFunction("\\text", {
+ numArgs: 1,
+ argTypes: ["text"],
+ greediness: 2,
+}, function(context, args) {
+ var body = args[0];
+ // Since the corresponding buildHTML/buildMathML function expects a
+ // list of elements, we normalize for different kinds of arguments
+ // TODO(emily): maybe this should be done somewhere else
+ var inner;
+ if (body.type === "ordgroup") {
+ inner = body.value;
+ } else {
+ inner = [body];
+ }
+
+ return {
+ type: "text",
+ body: inner,
+ };
+});
+
+// A two-argument custom color
+defineFunction("\\color", {
+ numArgs: 2,
+ allowedInText: true,
+ greediness: 3,
+ argTypes: ["color", "original"],
+}, function(context, args) {
+ var color = args[0];
+ var body = args[1];
+ // Normalize the different kinds of bodies (see \text above)
+ var inner;
+ if (body.type === "ordgroup") {
+ inner = body.value;
+ } else {
+ inner = [body];
+ }
+
+ return {
+ type: "color",
+ color: color.value,
+ value: inner,
+ };
+});
+
+// An overline
+defineFunction("\\overline", {
+ numArgs: 1,
+}, function(context, args) {
+ var body = args[0];
+ return {
+ type: "overline",
+ body: body,
+ };
+});
+
+// An underline
+defineFunction("\\underline", {
+ numArgs: 1,
+}, function(context, args) {
+ var body = args[0];
+ return {
+ type: "underline",
+ body: body,
+ };
+});
+
+// A box of the width and height
+defineFunction("\\rule", {
+ numArgs: 2,
+ numOptionalArgs: 1,
+ argTypes: ["size", "size", "size"],
+}, function(context, args) {
+ var shift = args[0];
+ var width = args[1];
+ var height = args[2];
+ return {
+ type: "rule",
+ shift: shift && shift.value,
+ width: width.value,
+ height: height.value,
+ };
+});
+
+defineFunction("\\kern", {
+ numArgs: 1,
+ argTypes: ["size"],
+}, function(context, args) {
+ return {
+ type: "kern",
+ dimension: args[0].value,
+ };
+});
+
+// A KaTeX logo
+defineFunction("\\KaTeX", {
+ numArgs: 0,
+}, function(context) {
+ return {
+ type: "katex",
+ };
+});
+
+defineFunction("\\phantom", {
+ numArgs: 1,
+}, function(context, args) {
+ var body = args[0];
+ var inner;
+ if (body.type === "ordgroup") {
+ inner = body.value;
+ } else {
+ inner = [body];
+ }
+
+ return {
+ type: "phantom",
+ value: inner,
+ };
+});
+
+// Extra data needed for the delimiter handler down below
+var delimiterSizes = {
+ "\\bigl" : {type: "open", size: 1},
+ "\\Bigl" : {type: "open", size: 2},
+ "\\biggl": {type: "open", size: 3},
+ "\\Biggl": {type: "open", size: 4},
+ "\\bigr" : {type: "close", size: 1},
+ "\\Bigr" : {type: "close", size: 2},
+ "\\biggr": {type: "close", size: 3},
+ "\\Biggr": {type: "close", size: 4},
+ "\\bigm" : {type: "rel", size: 1},
+ "\\Bigm" : {type: "rel", size: 2},
+ "\\biggm": {type: "rel", size: 3},
+ "\\Biggm": {type: "rel", size: 4},
+ "\\big" : {type: "textord", size: 1},
+ "\\Big" : {type: "textord", size: 2},
+ "\\bigg" : {type: "textord", size: 3},
+ "\\Bigg" : {type: "textord", size: 4},
+};
+
+var delimiters = [
+ "(", ")", "[", "\\lbrack", "]", "\\rbrack",
+ "\\{", "\\lbrace", "\\}", "\\rbrace",
+ "\\lfloor", "\\rfloor", "\\lceil", "\\rceil",
+ "<", ">", "\\langle", "\\rangle", "\\lt", "\\gt",
+ "\\lvert", "\\rvert", "\\lVert", "\\rVert",
+ "\\lgroup", "\\rgroup", "\\lmoustache", "\\rmoustache",
+ "/", "\\backslash",
+ "|", "\\vert", "\\|", "\\Vert",
+ "\\uparrow", "\\Uparrow",
+ "\\downarrow", "\\Downarrow",
+ "\\updownarrow", "\\Updownarrow",
+ ".",
+];
+
+var fontAliases = {
+ "\\Bbb": "\\mathbb",
+ "\\bold": "\\mathbf",
+ "\\frak": "\\mathfrak",
+};
+
+// Single-argument color functions
+defineFunction([
+ "\\blue", "\\orange", "\\pink", "\\red",
+ "\\green", "\\gray", "\\purple",
+ "\\blueA", "\\blueB", "\\blueC", "\\blueD", "\\blueE",
+ "\\tealA", "\\tealB", "\\tealC", "\\tealD", "\\tealE",
+ "\\greenA", "\\greenB", "\\greenC", "\\greenD", "\\greenE",
+ "\\goldA", "\\goldB", "\\goldC", "\\goldD", "\\goldE",
+ "\\redA", "\\redB", "\\redC", "\\redD", "\\redE",
+ "\\maroonA", "\\maroonB", "\\maroonC", "\\maroonD", "\\maroonE",
+ "\\purpleA", "\\purpleB", "\\purpleC", "\\purpleD", "\\purpleE",
+ "\\mintA", "\\mintB", "\\mintC",
+ "\\grayA", "\\grayB", "\\grayC", "\\grayD", "\\grayE",
+ "\\grayF", "\\grayG", "\\grayH", "\\grayI",
+ "\\kaBlue", "\\kaGreen",
+], {
+ numArgs: 1,
+ allowedInText: true,
+ greediness: 3,
+}, function(context, args) {
+ var body = args[0];
+ var atoms;
+ if (body.type === "ordgroup") {
+ atoms = body.value;
+ } else {
+ atoms = [body];
+ }
+
+ return {
+ type: "color",
+ color: "katex-" + context.funcName.slice(1),
+ value: atoms,
+ };
+});
+
+// There are 2 flags for operators; whether they produce limits in
+// displaystyle, and whether they are symbols and should grow in
+// displaystyle. These four groups cover the four possible choices.
+
+// No limits, not symbols
+defineFunction([
+ "\\arcsin", "\\arccos", "\\arctan", "\\arg", "\\cos", "\\cosh",
+ "\\cot", "\\coth", "\\csc", "\\deg", "\\dim", "\\exp", "\\hom",
+ "\\ker", "\\lg", "\\ln", "\\log", "\\sec", "\\sin", "\\sinh",
+ "\\tan", "\\tanh",
+], {
+ numArgs: 0,
+}, function(context) {
+ return {
+ type: "op",
+ limits: false,
+ symbol: false,
+ body: context.funcName,
+ };
+});
+
+// Limits, not symbols
+defineFunction([
+ "\\det", "\\gcd", "\\inf", "\\lim", "\\liminf", "\\limsup", "\\max",
+ "\\min", "\\Pr", "\\sup",
+], {
+ numArgs: 0,
+}, function(context) {
+ return {
+ type: "op",
+ limits: true,
+ symbol: false,
+ body: context.funcName,
+ };
+});
+
+// No limits, symbols
+defineFunction([
+ "\\int", "\\iint", "\\iiint", "\\oint",
+], {
+ numArgs: 0,
+}, function(context) {
+ return {
+ type: "op",
+ limits: false,
+ symbol: true,
+ body: context.funcName,
+ };
+});
+
+// Limits, symbols
+defineFunction([
+ "\\coprod", "\\bigvee", "\\bigwedge", "\\biguplus", "\\bigcap",
+ "\\bigcup", "\\intop", "\\prod", "\\sum", "\\bigotimes",
+ "\\bigoplus", "\\bigodot", "\\bigsqcup", "\\smallint",
+], {
+ numArgs: 0,
+}, function(context) {
+ return {
+ type: "op",
+ limits: true,
+ symbol: true,
+ body: context.funcName,
+ };
+});
+
+// Fractions
+defineFunction([
+ "\\dfrac", "\\frac", "\\tfrac",
+ "\\dbinom", "\\binom", "\\tbinom",
+], {
+ numArgs: 2,
+ greediness: 2,
+}, function(context, args) {
+ var numer = args[0];
+ var denom = args[1];
+ var hasBarLine;
+ var leftDelim = null;
+ var rightDelim = null;
+ var size = "auto";
+
+ switch (context.funcName) {
+ case "\\dfrac":
+ case "\\frac":
+ case "\\tfrac":
+ hasBarLine = true;
+ break;
+ case "\\dbinom":
+ case "\\binom":
+ case "\\tbinom":
+ hasBarLine = false;
+ leftDelim = "(";
+ rightDelim = ")";
+ break;
+ default:
+ throw new Error("Unrecognized genfrac command");
+ }
+
+ switch (context.funcName) {
+ case "\\dfrac":
+ case "\\dbinom":
+ size = "display";
+ break;
+ case "\\tfrac":
+ case "\\tbinom":
+ size = "text";
+ break;
+ }
+
+ return {
+ type: "genfrac",
+ numer: numer,
+ denom: denom,
+ hasBarLine: hasBarLine,
+ leftDelim: leftDelim,
+ rightDelim: rightDelim,
+ size: size,
+ };
+});
+
+// Left and right overlap functions
+defineFunction(["\\llap", "\\rlap"], {
+ numArgs: 1,
+ allowedInText: true,
+}, function(context, args) {
+ var body = args[0];
+ return {
+ type: context.funcName.slice(1),
+ body: body,
+ };
+});
+
+// Delimiter functions
+defineFunction([
+ "\\bigl", "\\Bigl", "\\biggl", "\\Biggl",
+ "\\bigr", "\\Bigr", "\\biggr", "\\Biggr",
+ "\\bigm", "\\Bigm", "\\biggm", "\\Biggm",
+ "\\big", "\\Big", "\\bigg", "\\Bigg",
+ "\\left", "\\right",
+], {
+ numArgs: 1,
+}, function(context, args) {
+ var delim = args[0];
+ if (!utils.contains(delimiters, delim.value)) {
+ throw new ParseError(
+ "Invalid delimiter: '" + delim.value + "' after '" +
+ context.funcName + "'", delim);
+ }
+
+ // \left and \right are caught somewhere in Parser.js, which is
+ // why this data doesn't match what is in buildHTML.
+ if (context.funcName === "\\left" || context.funcName === "\\right") {
+ return {
+ type: "leftright",
+ value: delim.value,
+ };
+ } else {
+ return {
+ type: "delimsizing",
+ size: delimiterSizes[context.funcName].size,
+ delimType: delimiterSizes[context.funcName].type,
+ value: delim.value,
+ };
+ }
+});
+
+// Sizing functions (handled in Parser.js explicitly, hence no handler)
+defineFunction([
+ "\\tiny", "\\scriptsize", "\\footnotesize", "\\small",
+ "\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge",
+], 0, null);
+
+// Style changing functions (handled in Parser.js explicitly, hence no
+// handler)
+defineFunction([
+ "\\displaystyle", "\\textstyle", "\\scriptstyle",
+ "\\scriptscriptstyle",
+], 0, null);
+
+defineFunction([
+ // styles
+ "\\mathrm", "\\mathit", "\\mathbf",
+
+ // families
+ "\\mathbb", "\\mathcal", "\\mathfrak", "\\mathscr", "\\mathsf",
+ "\\mathtt",
+
+ // aliases
+ "\\Bbb", "\\bold", "\\frak",
+], {
+ numArgs: 1,
+ greediness: 2,
+}, function(context, args) {
+ var body = args[0];
+ var func = context.funcName;
+ if (func in fontAliases) {
+ func = fontAliases[func];
+ }
+ return {
+ type: "font",
+ font: func.slice(1),
+ body: body,
+ };
+});
+
+// Accents
+defineFunction([
+ "\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve",
+ "\\check", "\\hat", "\\vec", "\\dot",
+ // We don't support expanding accents yet
+ // "\\widetilde", "\\widehat"
+], {
+ numArgs: 1,
+}, function(context, args) {
+ var base = args[0];
+ return {
+ type: "accent",
+ accent: context.funcName,
+ base: base,
+ };
+});
+
+// Infix generalized fractions
+defineFunction(["\\over", "\\choose"], {
+ numArgs: 0,
+ infix: true,
+}, function(context) {
+ var replaceWith;
+ switch (context.funcName) {
+ case "\\over":
+ replaceWith = "\\frac";
+ break;
+ case "\\choose":
+ replaceWith = "\\binom";
+ break;
+ default:
+ throw new Error("Unrecognized infix genfrac command");
+ }
+ return {
+ type: "infix",
+ replaceWith: replaceWith,
+ token: context.token,
+ };
+});
+
+// Row breaks for aligned data
+defineFunction(["\\\\", "\\cr"], {
+ numArgs: 0,
+ numOptionalArgs: 1,
+ argTypes: ["size"],
+}, function(context, args) {
+ var size = args[0];
+ return {
+ type: "cr",
+ size: size,
+ };
+});
+
+// Environment delimiters
+defineFunction(["\\begin", "\\end"], {
+ numArgs: 1,
+ argTypes: ["text"],
+}, function(context, args) {
+ var nameGroup = args[0];
+ if (nameGroup.type !== "ordgroup") {
+ throw new ParseError("Invalid environment name", nameGroup);
+ }
+ var name = "";
+ for (var i = 0; i < nameGroup.value.length; ++i) {
+ name += nameGroup.value[i].value;
+ }
+ return {
+ type: "environment",
+ name: name,
+ nameGroup: nameGroup,
+ };
+});
+
+},{"./ParseError":6,"./utils":25}],20:[function(require,module,exports){
+/**
+ * These objects store data about MathML nodes. This is the MathML equivalent
+ * of the types in domTree.js. Since MathML handles its own rendering, and
+ * since we're mainly using MathML to improve accessibility, we don't manage
+ * any of the styling state that the plain DOM nodes do.
+ *
+ * The `toNode` and `toMarkup` functions work simlarly to how they do in
+ * domTree.js, creating namespaced DOM nodes and HTML text markup respectively.
+ */
+
+var utils = require("./utils");
+
+/**
+ * This node represents a general purpose MathML node of any type. The
+ * constructor requires the type of node to create (for example, `"mo"` or
+ * `"mspace"`, corresponding to `<mo>` and `<mspace>` tags).
+ */
+function MathNode(type, children) {
+ this.type = type;
+ this.attributes = {};
+ this.children = children || [];
+}
+
+/**
+ * Sets an attribute on a MathML node. MathML depends on attributes to convey a
+ * semantic content, so this is used heavily.
+ */
+MathNode.prototype.setAttribute = function(name, value) {
+ this.attributes[name] = value;
+};
+
+/**
+ * Converts the math node into a MathML-namespaced DOM element.
+ */
+MathNode.prototype.toNode = function() {
+ var node = document.createElementNS(
+ "http://www.w3.org/1998/Math/MathML", this.type);
+
+ for (var attr in this.attributes) {
+ if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
+ node.setAttribute(attr, this.attributes[attr]);
+ }
+ }
+
+ for (var i = 0; i < this.children.length; i++) {
+ node.appendChild(this.children[i].toNode());
+ }
+
+ return node;
+};
+
+/**
+ * Converts the math node into an HTML markup string.
+ */
+MathNode.prototype.toMarkup = function() {
+ var markup = "<" + this.type;
+
+ // Add the attributes
+ for (var attr in this.attributes) {
+ if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
+ markup += " " + attr + "=\"";
+ markup += utils.escape(this.attributes[attr]);
+ markup += "\"";
+ }
+ }
+
+ markup += ">";
+
+ for (var i = 0; i < this.children.length; i++) {
+ markup += this.children[i].toMarkup();
+ }
+
+ markup += "</" + this.type + ">";
+
+ return markup;
+};
+
+/**
+ * This node represents a piece of text.
+ */
+function TextNode(text) {
+ this.text = text;
+}
+
+/**
+ * Converts the text node into a DOM text node.
+ */
+TextNode.prototype.toNode = function() {
+ return document.createTextNode(this.text);
+};
+
+/**
+ * Converts the text node into HTML markup (which is just the text itself).
+ */
+TextNode.prototype.toMarkup = function() {
+ return utils.escape(this.text);
+};
+
+module.exports = {
+ MathNode: MathNode,
+ TextNode: TextNode,
+};
+
+},{"./utils":25}],21:[function(require,module,exports){
+/**
+ * The resulting parse tree nodes of the parse tree.
+ *
+ * It is possible to provide position information, so that a ParseNode can
+ * fulfil a role similar to a Token in error reporting.
+ * For details on the corresponding properties see Token constructor.
+ * Providing such information can lead to better error reporting.
+ *
+ * @param {string} type type of node, like e.g. "ordgroup"
+ * @param {?object} value type-specific representation of the node
+ * @param {string} mode parse mode in action for this node,
+ * "math" or "text"
+ * @param {Token=} firstToken first token of the input for this node,
+ * will omit position information if unset
+ * @param {Token=} lastToken last token of the input for this node,
+ * will default to firstToken if unset
+ */
+function ParseNode(type, value, mode, firstToken, lastToken) {
+ this.type = type;
+ this.value = value;
+ this.mode = mode;
+ if (firstToken && (!lastToken || lastToken.lexer === firstToken.lexer)) {
+ this.lexer = firstToken.lexer;
+ this.start = firstToken.start;
+ this.end = (lastToken || firstToken).end;
+ }
+}
+
+module.exports = {
+ ParseNode: ParseNode,
+};
+
+
+},{}],22:[function(require,module,exports){
+/**
+ * Provides a single function for parsing an expression using a Parser
+ * TODO(emily): Remove this
+ */
+
+var Parser = require("./Parser");
+
+/**
+ * Parses an expression using a Parser, then returns the parsed result.
+ */
+var parseTree = function(toParse, settings) {
+ if (!(typeof toParse === 'string' || toParse instanceof String)) {
+ throw new TypeError('KaTeX can only parse string typed expression');
+ }
+ var parser = new Parser(toParse, settings);
+
+ return parser.parse();
+};
+
+module.exports = parseTree;
+
+},{"./Parser":7}],23:[function(require,module,exports){
+/**
+ * This file holds a list of all no-argument functions and single-character
+ * symbols (like 'a' or ';').
+ *
+ * For each of the symbols, there are three properties they can have:
+ * - font (required): the font to be used for this symbol. Either "main" (the
+ normal font), or "ams" (the ams fonts).
+ * - group (required): the ParseNode group type the symbol should have (i.e.
+ "textord", "mathord", etc).
+ See https://github.com/Khan/KaTeX/wiki/Examining-TeX#group-types
+ * - replace: the character that this symbol or function should be
+ * replaced with (i.e. "\phi" has a replace value of "\u03d5", the phi
+ * character in the main font).
+ *
+ * The outermost map in the table indicates what mode the symbols should be
+ * accepted in (e.g. "math" or "text").
+ */
+
+module.exports = {
+ math: {},
+ text: {},
+};
+
+function defineSymbol(mode, font, group, replace, name) {
+ module.exports[mode][name] = {
+ font: font,
+ group: group,
+ replace: replace,
+ };
+}
+
+// Some abbreviations for commonly used strings.
+// This helps minify the code, and also spotting typos using jshint.
+
+// modes:
+var math = "math";
+var text = "text";
+
+// fonts:
+var main = "main";
+var ams = "ams";
+
+// groups:
+var accent = "accent";
+var bin = "bin";
+var close = "close";
+var inner = "inner";
+var mathord = "mathord";
+var op = "op";
+var open = "open";
+var punct = "punct";
+var rel = "rel";
+var spacing = "spacing";
+var textord = "textord";
+
+// Now comes the symbol table
+
+// Relation Symbols
+defineSymbol(math, main, rel, "\u2261", "\\equiv");
+defineSymbol(math, main, rel, "\u227a", "\\prec");
+defineSymbol(math, main, rel, "\u227b", "\\succ");
+defineSymbol(math, main, rel, "\u223c", "\\sim");
+defineSymbol(math, main, rel, "\u22a5", "\\perp");
+defineSymbol(math, main, rel, "\u2aaf", "\\preceq");
+defineSymbol(math, main, rel, "\u2ab0", "\\succeq");
+defineSymbol(math, main, rel, "\u2243", "\\simeq");
+defineSymbol(math, main, rel, "\u2223", "\\mid");
+defineSymbol(math, main, rel, "\u226a", "\\ll");
+defineSymbol(math, main, rel, "\u226b", "\\gg");
+defineSymbol(math, main, rel, "\u224d", "\\asymp");
+defineSymbol(math, main, rel, "\u2225", "\\parallel");
+defineSymbol(math, main, rel, "\u22c8", "\\bowtie");
+defineSymbol(math, main, rel, "\u2323", "\\smile");
+defineSymbol(math, main, rel, "\u2291", "\\sqsubseteq");
+defineSymbol(math, main, rel, "\u2292", "\\sqsupseteq");
+defineSymbol(math, main, rel, "\u2250", "\\doteq");
+defineSymbol(math, main, rel, "\u2322", "\\frown");
+defineSymbol(math, main, rel, "\u220b", "\\ni");
+defineSymbol(math, main, rel, "\u221d", "\\propto");
+defineSymbol(math, main, rel, "\u22a2", "\\vdash");
+defineSymbol(math, main, rel, "\u22a3", "\\dashv");
+defineSymbol(math, main, rel, "\u220b", "\\owns");
+
+// Punctuation
+defineSymbol(math, main, punct, "\u002e", "\\ldotp");
+defineSymbol(math, main, punct, "\u22c5", "\\cdotp");
+
+// Misc Symbols
+defineSymbol(math, main, textord, "\u0023", "\\#");
+defineSymbol(math, main, textord, "\u0026", "\\&");
+defineSymbol(math, main, textord, "\u2135", "\\aleph");
+defineSymbol(math, main, textord, "\u2200", "\\forall");
+defineSymbol(math, main, textord, "\u210f", "\\hbar");
+defineSymbol(math, main, textord, "\u2203", "\\exists");
+defineSymbol(math, main, textord, "\u2207", "\\nabla");
+defineSymbol(math, main, textord, "\u266d", "\\flat");
+defineSymbol(math, main, textord, "\u2113", "\\ell");
+defineSymbol(math, main, textord, "\u266e", "\\natural");
+defineSymbol(math, main, textord, "\u2663", "\\clubsuit");
+defineSymbol(math, main, textord, "\u2118", "\\wp");
+defineSymbol(math, main, textord, "\u266f", "\\sharp");
+defineSymbol(math, main, textord, "\u2662", "\\diamondsuit");
+defineSymbol(math, main, textord, "\u211c", "\\Re");
+defineSymbol(math, main, textord, "\u2661", "\\heartsuit");
+defineSymbol(math, main, textord, "\u2111", "\\Im");
+defineSymbol(math, main, textord, "\u2660", "\\spadesuit");
+
+// Math and Text
+defineSymbol(math, main, textord, "\u2020", "\\dag");
+defineSymbol(math, main, textord, "\u2021", "\\ddag");
+
+// Large Delimiters
+defineSymbol(math, main, close, "\u23b1", "\\rmoustache");
+defineSymbol(math, main, open, "\u23b0", "\\lmoustache");
+defineSymbol(math, main, close, "\u27ef", "\\rgroup");
+defineSymbol(math, main, open, "\u27ee", "\\lgroup");
+
+// Binary Operators
+defineSymbol(math, main, bin, "\u2213", "\\mp");
+defineSymbol(math, main, bin, "\u2296", "\\ominus");
+defineSymbol(math, main, bin, "\u228e", "\\uplus");
+defineSymbol(math, main, bin, "\u2293", "\\sqcap");
+defineSymbol(math, main, bin, "\u2217", "\\ast");
+defineSymbol(math, main, bin, "\u2294", "\\sqcup");
+defineSymbol(math, main, bin, "\u25ef", "\\bigcirc");
+defineSymbol(math, main, bin, "\u2219", "\\bullet");
+defineSymbol(math, main, bin, "\u2021", "\\ddagger");
+defineSymbol(math, main, bin, "\u2240", "\\wr");
+defineSymbol(math, main, bin, "\u2a3f", "\\amalg");
+
+// Arrow Symbols
+defineSymbol(math, main, rel, "\u27f5", "\\longleftarrow");
+defineSymbol(math, main, rel, "\u21d0", "\\Leftarrow");
+defineSymbol(math, main, rel, "\u27f8", "\\Longleftarrow");
+defineSymbol(math, main, rel, "\u27f6", "\\longrightarrow");
+defineSymbol(math, main, rel, "\u21d2", "\\Rightarrow");
+defineSymbol(math, main, rel, "\u27f9", "\\Longrightarrow");
+defineSymbol(math, main, rel, "\u2194", "\\leftrightarrow");
+defineSymbol(math, main, rel, "\u27f7", "\\longleftrightarrow");
+defineSymbol(math, main, rel, "\u21d4", "\\Leftrightarrow");
+defineSymbol(math, main, rel, "\u27fa", "\\Longleftrightarrow");
+defineSymbol(math, main, rel, "\u21a6", "\\mapsto");
+defineSymbol(math, main, rel, "\u27fc", "\\longmapsto");
+defineSymbol(math, main, rel, "\u2197", "\\nearrow");
+defineSymbol(math, main, rel, "\u21a9", "\\hookleftarrow");
+defineSymbol(math, main, rel, "\u21aa", "\\hookrightarrow");
+defineSymbol(math, main, rel, "\u2198", "\\searrow");
+defineSymbol(math, main, rel, "\u21bc", "\\leftharpoonup");
+defineSymbol(math, main, rel, "\u21c0", "\\rightharpoonup");
+defineSymbol(math, main, rel, "\u2199", "\\swarrow");
+defineSymbol(math, main, rel, "\u21bd", "\\leftharpoondown");
+defineSymbol(math, main, rel, "\u21c1", "\\rightharpoondown");
+defineSymbol(math, main, rel, "\u2196", "\\nwarrow");
+defineSymbol(math, main, rel, "\u21cc", "\\rightleftharpoons");
+
+// AMS Negated Binary Relations
+defineSymbol(math, ams, rel, "\u226e", "\\nless");
+defineSymbol(math, ams, rel, "\ue010", "\\nleqslant");
+defineSymbol(math, ams, rel, "\ue011", "\\nleqq");
+defineSymbol(math, ams, rel, "\u2a87", "\\lneq");
+defineSymbol(math, ams, rel, "\u2268", "\\lneqq");
+defineSymbol(math, ams, rel, "\ue00c", "\\lvertneqq");
+defineSymbol(math, ams, rel, "\u22e6", "\\lnsim");
+defineSymbol(math, ams, rel, "\u2a89", "\\lnapprox");
+defineSymbol(math, ams, rel, "\u2280", "\\nprec");
+defineSymbol(math, ams, rel, "\u22e0", "\\npreceq");
+defineSymbol(math, ams, rel, "\u22e8", "\\precnsim");
+defineSymbol(math, ams, rel, "\u2ab9", "\\precnapprox");
+defineSymbol(math, ams, rel, "\u2241", "\\nsim");
+defineSymbol(math, ams, rel, "\ue006", "\\nshortmid");
+defineSymbol(math, ams, rel, "\u2224", "\\nmid");
+defineSymbol(math, ams, rel, "\u22ac", "\\nvdash");
+defineSymbol(math, ams, rel, "\u22ad", "\\nvDash");
+defineSymbol(math, ams, rel, "\u22ea", "\\ntriangleleft");
+defineSymbol(math, ams, rel, "\u22ec", "\\ntrianglelefteq");
+defineSymbol(math, ams, rel, "\u228a", "\\subsetneq");
+defineSymbol(math, ams, rel, "\ue01a", "\\varsubsetneq");
+defineSymbol(math, ams, rel, "\u2acb", "\\subsetneqq");
+defineSymbol(math, ams, rel, "\ue017", "\\varsubsetneqq");
+defineSymbol(math, ams, rel, "\u226f", "\\ngtr");
+defineSymbol(math, ams, rel, "\ue00f", "\\ngeqslant");
+defineSymbol(math, ams, rel, "\ue00e", "\\ngeqq");
+defineSymbol(math, ams, rel, "\u2a88", "\\gneq");
+defineSymbol(math, ams, rel, "\u2269", "\\gneqq");
+defineSymbol(math, ams, rel, "\ue00d", "\\gvertneqq");
+defineSymbol(math, ams, rel, "\u22e7", "\\gnsim");
+defineSymbol(math, ams, rel, "\u2a8a", "\\gnapprox");
+defineSymbol(math, ams, rel, "\u2281", "\\nsucc");
+defineSymbol(math, ams, rel, "\u22e1", "\\nsucceq");
+defineSymbol(math, ams, rel, "\u22e9", "\\succnsim");
+defineSymbol(math, ams, rel, "\u2aba", "\\succnapprox");
+defineSymbol(math, ams, rel, "\u2246", "\\ncong");
+defineSymbol(math, ams, rel, "\ue007", "\\nshortparallel");
+defineSymbol(math, ams, rel, "\u2226", "\\nparallel");
+defineSymbol(math, ams, rel, "\u22af", "\\nVDash");
+defineSymbol(math, ams, rel, "\u22eb", "\\ntriangleright");
+defineSymbol(math, ams, rel, "\u22ed", "\\ntrianglerighteq");
+defineSymbol(math, ams, rel, "\ue018", "\\nsupseteqq");
+defineSymbol(math, ams, rel, "\u228b", "\\supsetneq");
+defineSymbol(math, ams, rel, "\ue01b", "\\varsupsetneq");
+defineSymbol(math, ams, rel, "\u2acc", "\\supsetneqq");
+defineSymbol(math, ams, rel, "\ue019", "\\varsupsetneqq");
+defineSymbol(math, ams, rel, "\u22ae", "\\nVdash");
+defineSymbol(math, ams, rel, "\u2ab5", "\\precneqq");
+defineSymbol(math, ams, rel, "\u2ab6", "\\succneqq");
+defineSymbol(math, ams, rel, "\ue016", "\\nsubseteqq");
+defineSymbol(math, ams, bin, "\u22b4", "\\unlhd");
+defineSymbol(math, ams, bin, "\u22b5", "\\unrhd");
+
+// AMS Negated Arrows
+defineSymbol(math, ams, rel, "\u219a", "\\nleftarrow");
+defineSymbol(math, ams, rel, "\u219b", "\\nrightarrow");
+defineSymbol(math, ams, rel, "\u21cd", "\\nLeftarrow");
+defineSymbol(math, ams, rel, "\u21cf", "\\nRightarrow");
+defineSymbol(math, ams, rel, "\u21ae", "\\nleftrightarrow");
+defineSymbol(math, ams, rel, "\u21ce", "\\nLeftrightarrow");
+
+// AMS Misc
+defineSymbol(math, ams, rel, "\u25b3", "\\vartriangle");
+defineSymbol(math, ams, textord, "\u210f", "\\hslash");
+defineSymbol(math, ams, textord, "\u25bd", "\\triangledown");
+defineSymbol(math, ams, textord, "\u25ca", "\\lozenge");
+defineSymbol(math, ams, textord, "\u24c8", "\\circledS");
+defineSymbol(math, ams, textord, "\u00ae", "\\circledR");
+defineSymbol(math, ams, textord, "\u2221", "\\measuredangle");
+defineSymbol(math, ams, textord, "\u2204", "\\nexists");
+defineSymbol(math, ams, textord, "\u2127", "\\mho");
+defineSymbol(math, ams, textord, "\u2132", "\\Finv");
+defineSymbol(math, ams, textord, "\u2141", "\\Game");
+defineSymbol(math, ams, textord, "\u006b", "\\Bbbk");
+defineSymbol(math, ams, textord, "\u2035", "\\backprime");
+defineSymbol(math, ams, textord, "\u25b2", "\\blacktriangle");
+defineSymbol(math, ams, textord, "\u25bc", "\\blacktriangledown");
+defineSymbol(math, ams, textord, "\u25a0", "\\blacksquare");
+defineSymbol(math, ams, textord, "\u29eb", "\\blacklozenge");
+defineSymbol(math, ams, textord, "\u2605", "\\bigstar");
+defineSymbol(math, ams, textord, "\u2222", "\\sphericalangle");
+defineSymbol(math, ams, textord, "\u2201", "\\complement");
+defineSymbol(math, ams, textord, "\u00f0", "\\eth");
+defineSymbol(math, ams, textord, "\u2571", "\\diagup");
+defineSymbol(math, ams, textord, "\u2572", "\\diagdown");
+defineSymbol(math, ams, textord, "\u25a1", "\\square");
+defineSymbol(math, ams, textord, "\u25a1", "\\Box");
+defineSymbol(math, ams, textord, "\u25ca", "\\Diamond");
+defineSymbol(math, ams, textord, "\u00a5", "\\yen");
+defineSymbol(math, ams, textord, "\u2713", "\\checkmark");
+
+// AMS Hebrew
+defineSymbol(math, ams, textord, "\u2136", "\\beth");
+defineSymbol(math, ams, textord, "\u2138", "\\daleth");
+defineSymbol(math, ams, textord, "\u2137", "\\gimel");
+
+// AMS Greek
+defineSymbol(math, ams, textord, "\u03dd", "\\digamma");
+defineSymbol(math, ams, textord, "\u03f0", "\\varkappa");
+
+// AMS Delimiters
+defineSymbol(math, ams, open, "\u250c", "\\ulcorner");
+defineSymbol(math, ams, close, "\u2510", "\\urcorner");
+defineSymbol(math, ams, open, "\u2514", "\\llcorner");
+defineSymbol(math, ams, close, "\u2518", "\\lrcorner");
+
+// AMS Binary Relations
+defineSymbol(math, ams, rel, "\u2266", "\\leqq");
+defineSymbol(math, ams, rel, "\u2a7d", "\\leqslant");
+defineSymbol(math, ams, rel, "\u2a95", "\\eqslantless");
+defineSymbol(math, ams, rel, "\u2272", "\\lesssim");
+defineSymbol(math, ams, rel, "\u2a85", "\\lessapprox");
+defineSymbol(math, ams, rel, "\u224a", "\\approxeq");
+defineSymbol(math, ams, bin, "\u22d6", "\\lessdot");
+defineSymbol(math, ams, rel, "\u22d8", "\\lll");
+defineSymbol(math, ams, rel, "\u2276", "\\lessgtr");
+defineSymbol(math, ams, rel, "\u22da", "\\lesseqgtr");
+defineSymbol(math, ams, rel, "\u2a8b", "\\lesseqqgtr");
+defineSymbol(math, ams, rel, "\u2251", "\\doteqdot");
+defineSymbol(math, ams, rel, "\u2253", "\\risingdotseq");
+defineSymbol(math, ams, rel, "\u2252", "\\fallingdotseq");
+defineSymbol(math, ams, rel, "\u223d", "\\backsim");
+defineSymbol(math, ams, rel, "\u22cd", "\\backsimeq");
+defineSymbol(math, ams, rel, "\u2ac5", "\\subseteqq");
+defineSymbol(math, ams, rel, "\u22d0", "\\Subset");
+defineSymbol(math, ams, rel, "\u228f", "\\sqsubset");
+defineSymbol(math, ams, rel, "\u227c", "\\preccurlyeq");
+defineSymbol(math, ams, rel, "\u22de", "\\curlyeqprec");
+defineSymbol(math, ams, rel, "\u227e", "\\precsim");
+defineSymbol(math, ams, rel, "\u2ab7", "\\precapprox");
+defineSymbol(math, ams, rel, "\u22b2", "\\vartriangleleft");
+defineSymbol(math, ams, rel, "\u22b4", "\\trianglelefteq");
+defineSymbol(math, ams, rel, "\u22a8", "\\vDash");
+defineSymbol(math, ams, rel, "\u22aa", "\\Vvdash");
+defineSymbol(math, ams, rel, "\u2323", "\\smallsmile");
+defineSymbol(math, ams, rel, "\u2322", "\\smallfrown");
+defineSymbol(math, ams, rel, "\u224f", "\\bumpeq");
+defineSymbol(math, ams, rel, "\u224e", "\\Bumpeq");
+defineSymbol(math, ams, rel, "\u2267", "\\geqq");
+defineSymbol(math, ams, rel, "\u2a7e", "\\geqslant");
+defineSymbol(math, ams, rel, "\u2a96", "\\eqslantgtr");
+defineSymbol(math, ams, rel, "\u2273", "\\gtrsim");
+defineSymbol(math, ams, rel, "\u2a86", "\\gtrapprox");
+defineSymbol(math, ams, bin, "\u22d7", "\\gtrdot");
+defineSymbol(math, ams, rel, "\u22d9", "\\ggg");
+defineSymbol(math, ams, rel, "\u2277", "\\gtrless");
+defineSymbol(math, ams, rel, "\u22db", "\\gtreqless");
+defineSymbol(math, ams, rel, "\u2a8c", "\\gtreqqless");
+defineSymbol(math, ams, rel, "\u2256", "\\eqcirc");
+defineSymbol(math, ams, rel, "\u2257", "\\circeq");
+defineSymbol(math, ams, rel, "\u225c", "\\triangleq");
+defineSymbol(math, ams, rel, "\u223c", "\\thicksim");
+defineSymbol(math, ams, rel, "\u2248", "\\thickapprox");
+defineSymbol(math, ams, rel, "\u2ac6", "\\supseteqq");
+defineSymbol(math, ams, rel, "\u22d1", "\\Supset");
+defineSymbol(math, ams, rel, "\u2290", "\\sqsupset");
+defineSymbol(math, ams, rel, "\u227d", "\\succcurlyeq");
+defineSymbol(math, ams, rel, "\u22df", "\\curlyeqsucc");
+defineSymbol(math, ams, rel, "\u227f", "\\succsim");
+defineSymbol(math, ams, rel, "\u2ab8", "\\succapprox");
+defineSymbol(math, ams, rel, "\u22b3", "\\vartriangleright");
+defineSymbol(math, ams, rel, "\u22b5", "\\trianglerighteq");
+defineSymbol(math, ams, rel, "\u22a9", "\\Vdash");
+defineSymbol(math, ams, rel, "\u2223", "\\shortmid");
+defineSymbol(math, ams, rel, "\u2225", "\\shortparallel");
+defineSymbol(math, ams, rel, "\u226c", "\\between");
+defineSymbol(math, ams, rel, "\u22d4", "\\pitchfork");
+defineSymbol(math, ams, rel, "\u221d", "\\varpropto");
+defineSymbol(math, ams, rel, "\u25c0", "\\blacktriangleleft");
+defineSymbol(math, ams, rel, "\u2234", "\\therefore");
+defineSymbol(math, ams, rel, "\u220d", "\\backepsilon");
+defineSymbol(math, ams, rel, "\u25b6", "\\blacktriangleright");
+defineSymbol(math, ams, rel, "\u2235", "\\because");
+defineSymbol(math, ams, rel, "\u22d8", "\\llless");
+defineSymbol(math, ams, rel, "\u22d9", "\\gggtr");
+defineSymbol(math, ams, bin, "\u22b2", "\\lhd");
+defineSymbol(math, ams, bin, "\u22b3", "\\rhd");
+defineSymbol(math, ams, rel, "\u2242", "\\eqsim");
+defineSymbol(math, main, rel, "\u22c8", "\\Join");
+defineSymbol(math, ams, rel, "\u2251", "\\Doteq");
+
+// AMS Binary Operators
+defineSymbol(math, ams, bin, "\u2214", "\\dotplus");
+defineSymbol(math, ams, bin, "\u2216", "\\smallsetminus");
+defineSymbol(math, ams, bin, "\u22d2", "\\Cap");
+defineSymbol(math, ams, bin, "\u22d3", "\\Cup");
+defineSymbol(math, ams, bin, "\u2a5e", "\\doublebarwedge");
+defineSymbol(math, ams, bin, "\u229f", "\\boxminus");
+defineSymbol(math, ams, bin, "\u229e", "\\boxplus");
+defineSymbol(math, ams, bin, "\u22c7", "\\divideontimes");
+defineSymbol(math, ams, bin, "\u22c9", "\\ltimes");
+defineSymbol(math, ams, bin, "\u22ca", "\\rtimes");
+defineSymbol(math, ams, bin, "\u22cb", "\\leftthreetimes");
+defineSymbol(math, ams, bin, "\u22cc", "\\rightthreetimes");
+defineSymbol(math, ams, bin, "\u22cf", "\\curlywedge");
+defineSymbol(math, ams, bin, "\u22ce", "\\curlyvee");
+defineSymbol(math, ams, bin, "\u229d", "\\circleddash");
+defineSymbol(math, ams, bin, "\u229b", "\\circledast");
+defineSymbol(math, ams, bin, "\u22c5", "\\centerdot");
+defineSymbol(math, ams, bin, "\u22ba", "\\intercal");
+defineSymbol(math, ams, bin, "\u22d2", "\\doublecap");
+defineSymbol(math, ams, bin, "\u22d3", "\\doublecup");
+defineSymbol(math, ams, bin, "\u22a0", "\\boxtimes");
+
+// AMS Arrows
+defineSymbol(math, ams, rel, "\u21e2", "\\dashrightarrow");
+defineSymbol(math, ams, rel, "\u21e0", "\\dashleftarrow");
+defineSymbol(math, ams, rel, "\u21c7", "\\leftleftarrows");
+defineSymbol(math, ams, rel, "\u21c6", "\\leftrightarrows");
+defineSymbol(math, ams, rel, "\u21da", "\\Lleftarrow");
+defineSymbol(math, ams, rel, "\u219e", "\\twoheadleftarrow");
+defineSymbol(math, ams, rel, "\u21a2", "\\leftarrowtail");
+defineSymbol(math, ams, rel, "\u21ab", "\\looparrowleft");
+defineSymbol(math, ams, rel, "\u21cb", "\\leftrightharpoons");
+defineSymbol(math, ams, rel, "\u21b6", "\\curvearrowleft");
+defineSymbol(math, ams, rel, "\u21ba", "\\circlearrowleft");
+defineSymbol(math, ams, rel, "\u21b0", "\\Lsh");
+defineSymbol(math, ams, rel, "\u21c8", "\\upuparrows");
+defineSymbol(math, ams, rel, "\u21bf", "\\upharpoonleft");
+defineSymbol(math, ams, rel, "\u21c3", "\\downharpoonleft");
+defineSymbol(math, ams, rel, "\u22b8", "\\multimap");
+defineSymbol(math, ams, rel, "\u21ad", "\\leftrightsquigarrow");
+defineSymbol(math, ams, rel, "\u21c9", "\\rightrightarrows");
+defineSymbol(math, ams, rel, "\u21c4", "\\rightleftarrows");
+defineSymbol(math, ams, rel, "\u21a0", "\\twoheadrightarrow");
+defineSymbol(math, ams, rel, "\u21a3", "\\rightarrowtail");
+defineSymbol(math, ams, rel, "\u21ac", "\\looparrowright");
+defineSymbol(math, ams, rel, "\u21b7", "\\curvearrowright");
+defineSymbol(math, ams, rel, "\u21bb", "\\circlearrowright");
+defineSymbol(math, ams, rel, "\u21b1", "\\Rsh");
+defineSymbol(math, ams, rel, "\u21ca", "\\downdownarrows");
+defineSymbol(math, ams, rel, "\u21be", "\\upharpoonright");
+defineSymbol(math, ams, rel, "\u21c2", "\\downharpoonright");
+defineSymbol(math, ams, rel, "\u21dd", "\\rightsquigarrow");
+defineSymbol(math, ams, rel, "\u21dd", "\\leadsto");
+defineSymbol(math, ams, rel, "\u21db", "\\Rrightarrow");
+defineSymbol(math, ams, rel, "\u21be", "\\restriction");
+
+defineSymbol(math, main, textord, "\u2018", "`");
+defineSymbol(math, main, textord, "$", "\\$");
+defineSymbol(math, main, textord, "%", "\\%");
+defineSymbol(math, main, textord, "_", "\\_");
+defineSymbol(math, main, textord, "\u2220", "\\angle");
+defineSymbol(math, main, textord, "\u221e", "\\infty");
+defineSymbol(math, main, textord, "\u2032", "\\prime");
+defineSymbol(math, main, textord, "\u25b3", "\\triangle");
+defineSymbol(math, main, textord, "\u0393", "\\Gamma");
+defineSymbol(math, main, textord, "\u0394", "\\Delta");
+defineSymbol(math, main, textord, "\u0398", "\\Theta");
+defineSymbol(math, main, textord, "\u039b", "\\Lambda");
+defineSymbol(math, main, textord, "\u039e", "\\Xi");
+defineSymbol(math, main, textord, "\u03a0", "\\Pi");
+defineSymbol(math, main, textord, "\u03a3", "\\Sigma");
+defineSymbol(math, main, textord, "\u03a5", "\\Upsilon");
+defineSymbol(math, main, textord, "\u03a6", "\\Phi");
+defineSymbol(math, main, textord, "\u03a8", "\\Psi");
+defineSymbol(math, main, textord, "\u03a9", "\\Omega");
+defineSymbol(math, main, textord, "\u00ac", "\\neg");
+defineSymbol(math, main, textord, "\u00ac", "\\lnot");
+defineSymbol(math, main, textord, "\u22a4", "\\top");
+defineSymbol(math, main, textord, "\u22a5", "\\bot");
+defineSymbol(math, main, textord, "\u2205", "\\emptyset");
+defineSymbol(math, ams, textord, "\u2205", "\\varnothing");
+defineSymbol(math, main, mathord, "\u03b1", "\\alpha");
+defineSymbol(math, main, mathord, "\u03b2", "\\beta");
+defineSymbol(math, main, mathord, "\u03b3", "\\gamma");
+defineSymbol(math, main, mathord, "\u03b4", "\\delta");
+defineSymbol(math, main, mathord, "\u03f5", "\\epsilon");
+defineSymbol(math, main, mathord, "\u03b6", "\\zeta");
+defineSymbol(math, main, mathord, "\u03b7", "\\eta");
+defineSymbol(math, main, mathord, "\u03b8", "\\theta");
+defineSymbol(math, main, mathord, "\u03b9", "\\iota");
+defineSymbol(math, main, mathord, "\u03ba", "\\kappa");
+defineSymbol(math, main, mathord, "\u03bb", "\\lambda");
+defineSymbol(math, main, mathord, "\u03bc", "\\mu");
+defineSymbol(math, main, mathord, "\u03bd", "\\nu");
+defineSymbol(math, main, mathord, "\u03be", "\\xi");
+defineSymbol(math, main, mathord, "o", "\\omicron");
+defineSymbol(math, main, mathord, "\u03c0", "\\pi");
+defineSymbol(math, main, mathord, "\u03c1", "\\rho");
+defineSymbol(math, main, mathord, "\u03c3", "\\sigma");
+defineSymbol(math, main, mathord, "\u03c4", "\\tau");
+defineSymbol(math, main, mathord, "\u03c5", "\\upsilon");
+defineSymbol(math, main, mathord, "\u03d5", "\\phi");
+defineSymbol(math, main, mathord, "\u03c7", "\\chi");
+defineSymbol(math, main, mathord, "\u03c8", "\\psi");
+defineSymbol(math, main, mathord, "\u03c9", "\\omega");
+defineSymbol(math, main, mathord, "\u03b5", "\\varepsilon");
+defineSymbol(math, main, mathord, "\u03d1", "\\vartheta");
+defineSymbol(math, main, mathord, "\u03d6", "\\varpi");
+defineSymbol(math, main, mathord, "\u03f1", "\\varrho");
+defineSymbol(math, main, mathord, "\u03c2", "\\varsigma");
+defineSymbol(math, main, mathord, "\u03c6", "\\varphi");
+defineSymbol(math, main, bin, "\u2217", "*");
+defineSymbol(math, main, bin, "+", "+");
+defineSymbol(math, main, bin, "\u2212", "-");
+defineSymbol(math, main, bin, "\u22c5", "\\cdot");
+defineSymbol(math, main, bin, "\u2218", "\\circ");
+defineSymbol(math, main, bin, "\u00f7", "\\div");
+defineSymbol(math, main, bin, "\u00b1", "\\pm");
+defineSymbol(math, main, bin, "\u00d7", "\\times");
+defineSymbol(math, main, bin, "\u2229", "\\cap");
+defineSymbol(math, main, bin, "\u222a", "\\cup");
+defineSymbol(math, main, bin, "\u2216", "\\setminus");
+defineSymbol(math, main, bin, "\u2227", "\\land");
+defineSymbol(math, main, bin, "\u2228", "\\lor");
+defineSymbol(math, main, bin, "\u2227", "\\wedge");
+defineSymbol(math, main, bin, "\u2228", "\\vee");
+defineSymbol(math, main, textord, "\u221a", "\\surd");
+defineSymbol(math, main, open, "(", "(");
+defineSymbol(math, main, open, "[", "[");
+defineSymbol(math, main, open, "\u27e8", "\\langle");
+defineSymbol(math, main, open, "\u2223", "\\lvert");
+defineSymbol(math, main, open, "\u2225", "\\lVert");
+defineSymbol(math, main, close, ")", ")");
+defineSymbol(math, main, close, "]", "]");
+defineSymbol(math, main, close, "?", "?");
+defineSymbol(math, main, close, "!", "!");
+defineSymbol(math, main, close, "\u27e9", "\\rangle");
+defineSymbol(math, main, close, "\u2223", "\\rvert");
+defineSymbol(math, main, close, "\u2225", "\\rVert");
+defineSymbol(math, main, rel, "=", "=");
+defineSymbol(math, main, rel, "<", "<");
+defineSymbol(math, main, rel, ">", ">");
+defineSymbol(math, main, rel, ":", ":");
+defineSymbol(math, main, rel, "\u2248", "\\approx");
+defineSymbol(math, main, rel, "\u2245", "\\cong");
+defineSymbol(math, main, rel, "\u2265", "\\ge");
+defineSymbol(math, main, rel, "\u2265", "\\geq");
+defineSymbol(math, main, rel, "\u2190", "\\gets");
+defineSymbol(math, main, rel, ">", "\\gt");
+defineSymbol(math, main, rel, "\u2208", "\\in");
+defineSymbol(math, main, rel, "\u2209", "\\notin");
+defineSymbol(math, main, rel, "\u2282", "\\subset");
+defineSymbol(math, main, rel, "\u2283", "\\supset");
+defineSymbol(math, main, rel, "\u2286", "\\subseteq");
+defineSymbol(math, main, rel, "\u2287", "\\supseteq");
+defineSymbol(math, ams, rel, "\u2288", "\\nsubseteq");
+defineSymbol(math, ams, rel, "\u2289", "\\nsupseteq");
+defineSymbol(math, main, rel, "\u22a8", "\\models");
+defineSymbol(math, main, rel, "\u2190", "\\leftarrow");
+defineSymbol(math, main, rel, "\u2264", "\\le");
+defineSymbol(math, main, rel, "\u2264", "\\leq");
+defineSymbol(math, main, rel, "<", "\\lt");
+defineSymbol(math, main, rel, "\u2260", "\\ne");
+defineSymbol(math, main, rel, "\u2260", "\\neq");
+defineSymbol(math, main, rel, "\u2192", "\\rightarrow");
+defineSymbol(math, main, rel, "\u2192", "\\to");
+defineSymbol(math, ams, rel, "\u2271", "\\ngeq");
+defineSymbol(math, ams, rel, "\u2270", "\\nleq");
+defineSymbol(math, main, spacing, null, "\\!");
+defineSymbol(math, main, spacing, "\u00a0", "\\ ");
+defineSymbol(math, main, spacing, "\u00a0", "~");
+defineSymbol(math, main, spacing, null, "\\,");
+defineSymbol(math, main, spacing, null, "\\:");
+defineSymbol(math, main, spacing, null, "\\;");
+defineSymbol(math, main, spacing, null, "\\enspace");
+defineSymbol(math, main, spacing, null, "\\qquad");
+defineSymbol(math, main, spacing, null, "\\quad");
+defineSymbol(math, main, spacing, "\u00a0", "\\space");
+defineSymbol(math, main, punct, ",", ",");
+defineSymbol(math, main, punct, ";", ";");
+defineSymbol(math, main, punct, ":", "\\colon");
+defineSymbol(math, ams, bin, "\u22bc", "\\barwedge");
+defineSymbol(math, ams, bin, "\u22bb", "\\veebar");
+defineSymbol(math, main, bin, "\u2299", "\\odot");
+defineSymbol(math, main, bin, "\u2295", "\\oplus");
+defineSymbol(math, main, bin, "\u2297", "\\otimes");
+defineSymbol(math, main, textord, "\u2202", "\\partial");
+defineSymbol(math, main, bin, "\u2298", "\\oslash");
+defineSymbol(math, ams, bin, "\u229a", "\\circledcirc");
+defineSymbol(math, ams, bin, "\u22a1", "\\boxdot");
+defineSymbol(math, main, bin, "\u25b3", "\\bigtriangleup");
+defineSymbol(math, main, bin, "\u25bd", "\\bigtriangledown");
+defineSymbol(math, main, bin, "\u2020", "\\dagger");
+defineSymbol(math, main, bin, "\u22c4", "\\diamond");
+defineSymbol(math, main, bin, "\u22c6", "\\star");
+defineSymbol(math, main, bin, "\u25c3", "\\triangleleft");
+defineSymbol(math, main, bin, "\u25b9", "\\triangleright");
+defineSymbol(math, main, open, "{", "\\{");
+defineSymbol(math, main, close, "}", "\\}");
+defineSymbol(math, main, open, "{", "\\lbrace");
+defineSymbol(math, main, close, "}", "\\rbrace");
+defineSymbol(math, main, open, "[", "\\lbrack");
+defineSymbol(math, main, close, "]", "\\rbrack");
+defineSymbol(math, main, open, "\u230a", "\\lfloor");
+defineSymbol(math, main, close, "\u230b", "\\rfloor");
+defineSymbol(math, main, open, "\u2308", "\\lceil");
+defineSymbol(math, main, close, "\u2309", "\\rceil");
+defineSymbol(math, main, textord, "\\", "\\backslash");
+defineSymbol(math, main, textord, "\u2223", "|");
+defineSymbol(math, main, textord, "\u2223", "\\vert");
+defineSymbol(math, main, textord, "\u2225", "\\|");
+defineSymbol(math, main, textord, "\u2225", "\\Vert");
+defineSymbol(math, main, rel, "\u2191", "\\uparrow");
+defineSymbol(math, main, rel, "\u21d1", "\\Uparrow");
+defineSymbol(math, main, rel, "\u2193", "\\downarrow");
+defineSymbol(math, main, rel, "\u21d3", "\\Downarrow");
+defineSymbol(math, main, rel, "\u2195", "\\updownarrow");
+defineSymbol(math, main, rel, "\u21d5", "\\Updownarrow");
+defineSymbol(math, math, op, "\u2210", "\\coprod");
+defineSymbol(math, math, op, "\u22c1", "\\bigvee");
+defineSymbol(math, math, op, "\u22c0", "\\bigwedge");
+defineSymbol(math, math, op, "\u2a04", "\\biguplus");
+defineSymbol(math, math, op, "\u22c2", "\\bigcap");
+defineSymbol(math, math, op, "\u22c3", "\\bigcup");
+defineSymbol(math, math, op, "\u222b", "\\int");
+defineSymbol(math, math, op, "\u222b", "\\intop");
+defineSymbol(math, math, op, "\u222c", "\\iint");
+defineSymbol(math, math, op, "\u222d", "\\iiint");
+defineSymbol(math, math, op, "\u220f", "\\prod");
+defineSymbol(math, math, op, "\u2211", "\\sum");
+defineSymbol(math, math, op, "\u2a02", "\\bigotimes");
+defineSymbol(math, math, op, "\u2a01", "\\bigoplus");
+defineSymbol(math, math, op, "\u2a00", "\\bigodot");
+defineSymbol(math, math, op, "\u222e", "\\oint");
+defineSymbol(math, math, op, "\u2a06", "\\bigsqcup");
+defineSymbol(math, math, op, "\u222b", "\\smallint");
+defineSymbol(math, main, inner, "\u2026", "\\ldots");
+defineSymbol(math, main, inner, "\u22ef", "\\cdots");
+defineSymbol(math, main, inner, "\u22f1", "\\ddots");
+defineSymbol(math, main, textord, "\u22ee", "\\vdots");
+defineSymbol(math, main, accent, "\u00b4", "\\acute");
+defineSymbol(math, main, accent, "\u0060", "\\grave");
+defineSymbol(math, main, accent, "\u00a8", "\\ddot");
+defineSymbol(math, main, accent, "\u007e", "\\tilde");
+defineSymbol(math, main, accent, "\u00af", "\\bar");
+defineSymbol(math, main, accent, "\u02d8", "\\breve");
+defineSymbol(math, main, accent, "\u02c7", "\\check");
+defineSymbol(math, main, accent, "\u005e", "\\hat");
+defineSymbol(math, main, accent, "\u20d7", "\\vec");
+defineSymbol(math, main, accent, "\u02d9", "\\dot");
+defineSymbol(math, main, mathord, "\u0131", "\\imath");
+defineSymbol(math, main, mathord, "\u0237", "\\jmath");
+
+defineSymbol(text, main, textord, "\u2013", "--");
+defineSymbol(text, main, textord, "\u2014", "---");
+defineSymbol(text, main, textord, "\u2018", "`");
+defineSymbol(text, main, textord, "\u2019", "'");
+defineSymbol(text, main, textord, "\u201c", "``");
+defineSymbol(text, main, textord, "\u201d", "''");
+defineSymbol(math, main, textord, "\u00b0", "\\degree");
+defineSymbol(text, main, textord, "\u00b0", "\\degree");
+defineSymbol(math, main, mathord, "\u00a3", "\\pounds");
+defineSymbol(math, ams, textord, "\u2720", "\\maltese");
+defineSymbol(text, ams, textord, "\u2720", "\\maltese");
+
+defineSymbol(text, main, spacing, "\u00a0", "\\ ");
+defineSymbol(text, main, spacing, "\u00a0", " ");
+defineSymbol(text, main, spacing, "\u00a0", "~");
+
+// There are lots of symbols which are the same, so we add them in afterwards.
+var i;
+var ch;
+
+// All of these are textords in math mode
+var mathTextSymbols = "0123456789/@.\"";
+for (i = 0; i < mathTextSymbols.length; i++) {
+ ch = mathTextSymbols.charAt(i);
+ defineSymbol(math, main, textord, ch, ch);
+}
+
+// All of these are textords in text mode
+var textSymbols = "0123456789!@*()-=+[]\";:?/.,";
+for (i = 0; i < textSymbols.length; i++) {
+ ch = textSymbols.charAt(i);
+ defineSymbol(text, main, textord, ch, ch);
+}
+
+// All of these are textords in text mode, and mathords in math mode
+var letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+for (i = 0; i < letters.length; i++) {
+ ch = letters.charAt(i);
+ defineSymbol(math, main, mathord, ch, ch);
+ defineSymbol(text, main, textord, ch, ch);
+}
+
+// Latin-1 letters
+for (i = 0x00C0; i <= 0x00D6; i++) {
+ ch = String.fromCharCode(i);
+ defineSymbol(text, main, textord, ch, ch);
+}
+
+for (i = 0x00D8; i <= 0x00F6; i++) {
+ ch = String.fromCharCode(i);
+ defineSymbol(text, main, textord, ch, ch);
+}
+
+for (i = 0x00F8; i <= 0x00FF; i++) {
+ ch = String.fromCharCode(i);
+ defineSymbol(text, main, textord, ch, ch);
+}
+
+// Cyrillic
+for (i = 0x0410; i <= 0x044F; i++) {
+ ch = String.fromCharCode(i);
+ defineSymbol(text, main, textord, ch, ch);
+}
+
+},{}],24:[function(require,module,exports){
+var hangulRegex = /[\uAC00-\uD7AF]/;
+
+// This regex combines
+// - Hiragana: [\u3040-\u309F]
+// - Katakana: [\u30A0-\u30FF]
+// - CJK ideograms: [\u4E00-\u9FAF]
+// - Hangul syllables: [\uAC00-\uD7AF]
+// Notably missing are halfwidth Katakana and Romanji glyphs.
+var cjkRegex =
+ /[\u3040-\u309F]|[\u30A0-\u30FF]|[\u4E00-\u9FAF]|[\uAC00-\uD7AF]/;
+
+module.exports = {
+ cjkRegex: cjkRegex,
+ hangulRegex: hangulRegex,
+};
+
+},{}],25:[function(require,module,exports){
+/**
+ * This file contains a list of utility functions which are useful in other
+ * files.
+ */
+
+/**
+ * Provide an `indexOf` function which works in IE8, but defers to native if
+ * possible.
+ */
+var nativeIndexOf = Array.prototype.indexOf;
+var indexOf = function(list, elem) {
+ if (list == null) {
+ return -1;
+ }
+ if (nativeIndexOf && list.indexOf === nativeIndexOf) {
+ return list.indexOf(elem);
+ }
+ var i = 0;
+ var l = list.length;
+ for (; i < l; i++) {
+ if (list[i] === elem) {
+ return i;
+ }
+ }
+ return -1;
+};
+
+/**
+ * Return whether an element is contained in a list
+ */
+var contains = function(list, elem) {
+ return indexOf(list, elem) !== -1;
+};
+
+/**
+ * Provide a default value if a setting is undefined
+ */
+var deflt = function(setting, defaultIfUndefined) {
+ return setting === undefined ? defaultIfUndefined : setting;
+};
+
+// hyphenate and escape adapted from Facebook's React under Apache 2 license
+
+var uppercase = /([A-Z])/g;
+var hyphenate = function(str) {
+ return str.replace(uppercase, "-$1").toLowerCase();
+};
+
+var ESCAPE_LOOKUP = {
+ "&": "&amp;",
+ ">": "&gt;",
+ "<": "&lt;",
+ "\"": "&quot;",
+ "'": "&#x27;",
+};
+
+var ESCAPE_REGEX = /[&><"']/g;
+
+function escaper(match) {
+ return ESCAPE_LOOKUP[match];
+}
+
+/**
+ * Escapes text to prevent scripting attacks.
+ *
+ * @param {*} text Text value to escape.
+ * @return {string} An escaped string.
+ */
+function escape(text) {
+ return ("" + text).replace(ESCAPE_REGEX, escaper);
+}
+
+/**
+ * A function to set the text content of a DOM element in all supported
+ * browsers. Note that we don't define this if there is no document.
+ */
+var setTextContent;
+if (typeof document !== "undefined") {
+ var testNode = document.createElement("span");
+ if ("textContent" in testNode) {
+ setTextContent = function(node, text) {
+ node.textContent = text;
+ };
+ } else {
+ setTextContent = function(node, text) {
+ node.innerText = text;
+ };
+ }
+}
+
+/**
+ * A function to clear a node.
+ */
+function clearNode(node) {
+ setTextContent(node, "");
+}
+
+module.exports = {
+ contains: contains,
+ deflt: deflt,
+ escape: escape,
+ hyphenate: hyphenate,
+ indexOf: indexOf,
+ setTextContent: setTextContent,
+ clearNode: clearNode,
+};
+
+},{}]},{},[1])(1)
+}); \ No newline at end of file
diff --git a/vendor/assets/javascripts/timeago.js b/vendor/assets/javascripts/timeago.js
new file mode 100644
index 00000000000..0eb6f7967a5
--- /dev/null
+++ b/vendor/assets/javascripts/timeago.js
@@ -0,0 +1,237 @@
+/**
+ * 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/vue.full.js b/vendor/assets/javascripts/vue.full.js
index 7ae95897a01..ea15bfac416 100644
--- a/vendor/assets/javascripts/vue.full.js
+++ b/vendor/assets/javascripts/vue.full.js
@@ -1,10073 +1,7515 @@
/*!
- * Vue.js v1.0.26
- * (c) 2016 Evan You
+ * Vue.js v2.0.3
+ * (c) 2014-2016 Evan You
* Released under the MIT License.
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Vue = factory());
-}(this, function () { 'use strict';
+}(this, (function () { 'use strict';
- function set(obj, key, val) {
- if (hasOwn(obj, key)) {
- obj[key] = val;
- return;
- }
- if (obj._isVue) {
- set(obj._data, key, val);
- return;
- }
- var ob = obj.__ob__;
- if (!ob) {
- obj[key] = val;
- return;
- }
- ob.convert(key, val);
- ob.dep.notify();
- if (ob.vms) {
- var i = ob.vms.length;
- while (i--) {
- var vm = ob.vms[i];
- vm._proxy(key);
- vm._digest();
- }
- }
- return val;
- }
+/* */
- /**
- * Delete a property and trigger change if necessary.
- *
- * @param {Object} obj
- * @param {String} key
- */
+/**
+ * Convert a value to a string that is actually rendered.
+ */
+function _toString (val) {
+ return val == null
+ ? ''
+ : typeof val === 'object'
+ ? JSON.stringify(val, null, 2)
+ : String(val)
+}
+
+/**
+ * Convert a input value to a number for persistence.
+ * If the conversion fails, return original string.
+ */
+function toNumber (val) {
+ var n = parseFloat(val, 10);
+ return (n || n === 0) ? n : val
+}
+
+/**
+ * Make a map and return a function for checking if a key
+ * is in that map.
+ */
+function makeMap (
+ str,
+ expectsLowerCase
+) {
+ var map = Object.create(null);
+ var list = str.split(',');
+ for (var i = 0; i < list.length; i++) {
+ map[list[i]] = true;
+ }
+ return expectsLowerCase
+ ? function (val) { return map[val.toLowerCase()]; }
+ : function (val) { return map[val]; }
+}
+
+/**
+ * Check if a tag is a built-in tag.
+ */
+var isBuiltInTag = makeMap('slot,component', true);
- function del(obj, key) {
- if (!hasOwn(obj, key)) {
- return;
- }
- delete obj[key];
- var ob = obj.__ob__;
- if (!ob) {
- if (obj._isVue) {
- delete obj._data[key];
- obj._digest();
- }
- return;
- }
- ob.dep.notify();
- if (ob.vms) {
- var i = ob.vms.length;
- while (i--) {
- var vm = ob.vms[i];
- vm._unproxy(key);
- vm._digest();
- }
+/**
+ * Remove an item from an array
+ */
+function remove$1 (arr, item) {
+ if (arr.length) {
+ var index = arr.indexOf(item);
+ if (index > -1) {
+ return arr.splice(index, 1)
}
}
+}
- var hasOwnProperty = Object.prototype.hasOwnProperty;
- /**
- * Check whether the object has the property.
- *
- * @param {Object} obj
- * @param {String} key
- * @return {Boolean}
- */
+/**
+ * Check whether the object has the property.
+ */
+var hasOwnProperty = Object.prototype.hasOwnProperty;
+function hasOwn (obj, key) {
+ return hasOwnProperty.call(obj, key)
+}
+
+/**
+ * Check if value is primitive
+ */
+function isPrimitive (value) {
+ return typeof value === 'string' || typeof value === 'number'
+}
- function hasOwn(obj, key) {
- return hasOwnProperty.call(obj, key);
+/**
+ * Create a cached version of a pure function.
+ */
+function cached (fn) {
+ var cache = Object.create(null);
+ return function cachedFn (str) {
+ var hit = cache[str];
+ return hit || (cache[str] = fn(str))
}
+}
- /**
- * Check if an expression is a literal value.
- *
- * @param {String} exp
- * @return {Boolean}
- */
+/**
+ * Camelize a hyphen-delmited string.
+ */
+var camelizeRE = /-(\w)/g;
+var camelize = cached(function (str) {
+ return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })
+});
- var literalValueRE = /^\s?(true|false|-?[\d\.]+|'[^']*'|"[^"]*")\s?$/;
+/**
+ * Capitalize a string.
+ */
+var capitalize = cached(function (str) {
+ return str.charAt(0).toUpperCase() + str.slice(1)
+});
- function isLiteral(exp) {
- return literalValueRE.test(exp);
+/**
+ * Hyphenate a camelCase string.
+ */
+var hyphenateRE = /([^-])([A-Z])/g;
+var hyphenate = cached(function (str) {
+ return str
+ .replace(hyphenateRE, '$1-$2')
+ .replace(hyphenateRE, '$1-$2')
+ .toLowerCase()
+});
+
+/**
+ * Simple bind, faster than native
+ */
+function bind$1 (fn, ctx) {
+ function boundFn (a) {
+ var l = arguments.length;
+ return l
+ ? l > 1
+ ? fn.apply(ctx, arguments)
+ : fn.call(ctx, a)
+ : fn.call(ctx)
+ }
+ // record original fn length
+ boundFn._length = fn.length;
+ return boundFn
+}
+
+/**
+ * Convert an Array-like object to a real Array.
+ */
+function toArray (list, start) {
+ start = start || 0;
+ var i = list.length - start;
+ var ret = new Array(i);
+ while (i--) {
+ ret[i] = list[i + start];
+ }
+ return ret
+}
+
+/**
+ * Mix properties into target object.
+ */
+function extend (to, _from) {
+ for (var key in _from) {
+ to[key] = _from[key];
}
+ return to
+}
- /**
- * Check if a string starts with $ or _
- *
- * @param {String} str
- * @return {Boolean}
- */
+/**
+ * Quick object check - this is primarily used to tell
+ * Objects from primitive values when we know the value
+ * is a JSON-compliant type.
+ */
+function isObject (obj) {
+ return obj !== null && typeof obj === 'object'
+}
- function isReserved(str) {
- var c = (str + '').charCodeAt(0);
- return c === 0x24 || c === 0x5F;
+/**
+ * Strict object type check. Only returns true
+ * for plain JavaScript objects.
+ */
+var toString = Object.prototype.toString;
+var OBJECT_STRING = '[object Object]';
+function isPlainObject (obj) {
+ return toString.call(obj) === OBJECT_STRING
+}
+
+/**
+ * Merge an Array of Objects into a single Object.
+ */
+function toObject (arr) {
+ var res = {};
+ for (var i = 0; i < arr.length; i++) {
+ if (arr[i]) {
+ extend(res, arr[i]);
+ }
}
+ return res
+}
- /**
- * Guard text output, make sure undefined outputs
- * empty string
- *
- * @param {*} value
- * @return {String}
- */
+/**
+ * Perform no operation.
+ */
+function noop () {}
- function _toString(value) {
- return value == null ? '' : value.toString();
- }
+/**
+ * Always return false.
+ */
+var no = function () { return false; };
- /**
- * Check and convert possible numeric strings to numbers
- * before setting back to data
- *
- * @param {*} value
- * @return {*|Number}
- */
+/**
+ * Generate a static keys string from compiler modules.
+ */
+function genStaticKeys (modules) {
+ return modules.reduce(function (keys, m) {
+ return keys.concat(m.staticKeys || [])
+ }, []).join(',')
+}
+
+/**
+ * Check if two values are loosely equal - that is,
+ * if they are plain objects, do they have the same shape?
+ */
+function looseEqual (a, b) {
+ /* eslint-disable eqeqeq */
+ return a == b || (
+ isObject(a) && isObject(b)
+ ? JSON.stringify(a) === JSON.stringify(b)
+ : false
+ )
+ /* eslint-enable eqeqeq */
+}
- function toNumber(value) {
- if (typeof value !== 'string') {
- return value;
- } else {
- var parsed = Number(value);
- return isNaN(parsed) ? value : parsed;
- }
+function looseIndexOf (arr, val) {
+ for (var i = 0; i < arr.length; i++) {
+ if (looseEqual(arr[i], val)) { return i }
}
+ return -1
+}
- /**
- * Convert string boolean literals into real booleans.
- *
- * @param {*} value
- * @return {*|Boolean}
- */
-
- function toBoolean(value) {
- return value === 'true' ? true : value === 'false' ? false : value;
- }
+/* */
+var config = {
/**
- * Strip quotes from a string
- *
- * @param {String} str
- * @return {String | false}
+ * Option merge strategies (used in core/util/options)
*/
-
- function stripQuotes(str) {
- var a = str.charCodeAt(0);
- var b = str.charCodeAt(str.length - 1);
- return a === b && (a === 0x22 || a === 0x27) ? str.slice(1, -1) : str;
- }
+ optionMergeStrategies: Object.create(null),
/**
- * Camelize a hyphen-delmited string.
- *
- * @param {String} str
- * @return {String}
+ * Whether to suppress warnings.
*/
-
- var camelizeRE = /-(\w)/g;
-
- function camelize(str) {
- return str.replace(camelizeRE, toUpper);
- }
-
- function toUpper(_, c) {
- return c ? c.toUpperCase() : '';
- }
+ silent: false,
/**
- * Hyphenate a camelCase string.
- *
- * @param {String} str
- * @return {String}
+ * Whether to enable devtools
*/
-
- var hyphenateRE = /([a-z\d])([A-Z])/g;
-
- function hyphenate(str) {
- return str.replace(hyphenateRE, '$1-$2').toLowerCase();
- }
+ devtools: "development" !== 'production',
/**
- * Converts hyphen/underscore/slash delimitered names into
- * camelized classNames.
- *
- * e.g. my-component => MyComponent
- * some_else => SomeElse
- * some/comp => SomeComp
- *
- * @param {String} str
- * @return {String}
+ * Error handler for watcher errors
*/
-
- var classifyRE = /(?:^|[-_\/])(\w)/g;
-
- function classify(str) {
- return str.replace(classifyRE, toUpper);
- }
+ errorHandler: null,
/**
- * Simple bind, faster than native
- *
- * @param {Function} fn
- * @param {Object} ctx
- * @return {Function}
+ * Ignore certain custom elements
*/
-
- function bind(fn, ctx) {
- return function (a) {
- var l = arguments.length;
- return l ? l > 1 ? fn.apply(ctx, arguments) : fn.call(ctx, a) : fn.call(ctx);
- };
- }
+ ignoredElements: null,
/**
- * Convert an Array-like object to a real Array.
- *
- * @param {Array-like} list
- * @param {Number} [start] - start index
- * @return {Array}
+ * Custom user key aliases for v-on
*/
-
- function toArray(list, start) {
- start = start || 0;
- var i = list.length - start;
- var ret = new Array(i);
- while (i--) {
- ret[i] = list[i + start];
- }
- return ret;
- }
+ keyCodes: Object.create(null),
/**
- * Mix properties into target object.
- *
- * @param {Object} to
- * @param {Object} from
+ * Check if a tag is reserved so that it cannot be registered as a
+ * component. This is platform-dependent and may be overwritten.
*/
-
- function extend(to, from) {
- var keys = Object.keys(from);
- var i = keys.length;
- while (i--) {
- to[keys[i]] = from[keys[i]];
- }
- return to;
- }
+ isReservedTag: no,
/**
- * Quick object check - this is primarily used to tell
- * Objects from primitive values when we know the value
- * is a JSON-compliant type.
- *
- * @param {*} obj
- * @return {Boolean}
+ * Check if a tag is an unknown element.
+ * Platform-dependent.
*/
-
- function isObject(obj) {
- return obj !== null && typeof obj === 'object';
- }
+ isUnknownElement: no,
/**
- * Strict object type check. Only returns true
- * for plain JavaScript objects.
- *
- * @param {*} obj
- * @return {Boolean}
+ * Get the namespace of an element
*/
-
- var toString = Object.prototype.toString;
- var OBJECT_STRING = '[object Object]';
-
- function isPlainObject(obj) {
- return toString.call(obj) === OBJECT_STRING;
- }
+ getTagNamespace: noop,
/**
- * Array type check.
- *
- * @param {*} obj
- * @return {Boolean}
+ * Check if an attribute must be bound using property, e.g. value
+ * Platform-dependent.
*/
-
- var isArray = Array.isArray;
+ mustUseProp: no,
/**
- * Define a property.
- *
- * @param {Object} obj
- * @param {String} key
- * @param {*} val
- * @param {Boolean} [enumerable]
+ * List of asset types that a component can own.
*/
-
- function def(obj, key, val, enumerable) {
- Object.defineProperty(obj, key, {
- value: val,
- enumerable: !!enumerable,
- writable: true,
- configurable: true
- });
- }
+ _assetTypes: [
+ 'component',
+ 'directive',
+ 'filter'
+ ],
/**
- * Debounce a function so it only gets called after the
- * input stops arriving after the given wait period.
- *
- * @param {Function} func
- * @param {Number} wait
- * @return {Function} - the debounced function
+ * List of lifecycle hooks.
*/
-
- function _debounce(func, wait) {
- var timeout, args, context, timestamp, result;
- var later = function later() {
- var last = Date.now() - timestamp;
- if (last < wait && last >= 0) {
- timeout = setTimeout(later, wait - last);
- } else {
- timeout = null;
- result = func.apply(context, args);
- if (!timeout) context = args = null;
- }
- };
- return function () {
- context = this;
- args = arguments;
- timestamp = Date.now();
- if (!timeout) {
- timeout = setTimeout(later, wait);
- }
- return result;
- };
- }
+ _lifecycleHooks: [
+ 'beforeCreate',
+ 'created',
+ 'beforeMount',
+ 'mounted',
+ 'beforeUpdate',
+ 'updated',
+ 'beforeDestroy',
+ 'destroyed',
+ 'activated',
+ 'deactivated'
+ ],
/**
- * Manual indexOf because it's slightly faster than
- * native.
- *
- * @param {Array} arr
- * @param {*} obj
+ * Max circular updates allowed in a scheduler flush cycle.
*/
-
- function indexOf(arr, obj) {
- var i = arr.length;
- while (i--) {
- if (arr[i] === obj) return i;
- }
- return -1;
- }
+ _maxUpdateCount: 100,
/**
- * Make a cancellable version of an async callback.
- *
- * @param {Function} fn
- * @return {Function}
+ * Server rendering?
*/
+ _isServer: "client" === 'server'
+};
- function cancellable(fn) {
- var cb = function cb() {
- if (!cb.cancelled) {
- return fn.apply(this, arguments);
- }
- };
- cb.cancel = function () {
- cb.cancelled = true;
- };
- return cb;
- }
+/* */
- /**
- * Check if two values are loosely equal - that is,
- * if they are plain objects, do they have the same shape?
- *
- * @param {*} a
- * @param {*} b
- * @return {Boolean}
- */
+/**
+ * Check if a string starts with $ or _
+ */
+function isReserved (str) {
+ var c = (str + '').charCodeAt(0);
+ return c === 0x24 || c === 0x5F
+}
- function looseEqual(a, b) {
- /* eslint-disable eqeqeq */
- return a == b || (isObject(a) && isObject(b) ? JSON.stringify(a) === JSON.stringify(b) : false);
- /* eslint-enable eqeqeq */
+/**
+ * Define a property.
+ */
+function def (obj, key, val, enumerable) {
+ Object.defineProperty(obj, key, {
+ value: val,
+ enumerable: !!enumerable,
+ writable: true,
+ configurable: true
+ });
+}
+
+/**
+ * Parse simple path.
+ */
+var bailRE = /[^\w\.\$]/;
+function parsePath (path) {
+ if (bailRE.test(path)) {
+ return
+ } else {
+ var segments = path.split('.');
+ return function (obj) {
+ for (var i = 0; i < segments.length; i++) {
+ if (!obj) { return }
+ obj = obj[segments[i]];
+ }
+ return obj
+ }
}
+}
- var hasProto = ('__proto__' in {});
+/* */
+/* globals MutationObserver */
- // Browser environment sniffing
- var inBrowser = typeof window !== 'undefined' && Object.prototype.toString.call(window) !== '[object Object]';
+// can we use __proto__?
+var hasProto = '__proto__' in {};
- // detect devtools
- var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
+// Browser environment sniffing
+var inBrowser =
+ typeof window !== 'undefined' &&
+ Object.prototype.toString.call(window) !== '[object Object]';
- // UA sniffing for working around browser-specific quirks
- var UA = inBrowser && window.navigator.userAgent.toLowerCase();
- var isIE = UA && UA.indexOf('trident') > 0;
- var isIE9 = UA && UA.indexOf('msie 9.0') > 0;
- var isAndroid = UA && UA.indexOf('android') > 0;
- var isIos = UA && /(iphone|ipad|ipod|ios)/i.test(UA);
- var iosVersionMatch = isIos && UA.match(/os ([\d_]+)/);
- var iosVersion = iosVersionMatch && iosVersionMatch[1].split('_');
+var UA = inBrowser && window.navigator.userAgent.toLowerCase();
+var isIE = UA && /msie|trident/.test(UA);
+var isIE9 = UA && UA.indexOf('msie 9.0') > 0;
+var isEdge = UA && UA.indexOf('edge/') > 0;
+var isAndroid = UA && UA.indexOf('android') > 0;
+var isIOS = UA && /iphone|ipad|ipod|ios/.test(UA);
- // detecting iOS UIWebView by indexedDB
- var hasMutationObserverBug = iosVersion && Number(iosVersion[0]) >= 9 && Number(iosVersion[1]) >= 3 && !window.indexedDB;
+// detect devtools
+var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
- var transitionProp = undefined;
- var transitionEndEvent = undefined;
- var animationProp = undefined;
- var animationEndEvent = undefined;
+/* istanbul ignore next */
+function isNative (Ctor) {
+ return /native code/.test(Ctor.toString())
+}
- // Transition property/event sniffing
- if (inBrowser && !isIE9) {
- var isWebkitTrans = window.ontransitionend === undefined && window.onwebkittransitionend !== undefined;
- var isWebkitAnim = window.onanimationend === undefined && window.onwebkitanimationend !== undefined;
- transitionProp = isWebkitTrans ? 'WebkitTransition' : 'transition';
- transitionEndEvent = isWebkitTrans ? 'webkitTransitionEnd' : 'transitionend';
- animationProp = isWebkitAnim ? 'WebkitAnimation' : 'animation';
- animationEndEvent = isWebkitAnim ? 'webkitAnimationEnd' : 'animationend';
+/**
+ * Defer a task to execute it asynchronously.
+ */
+var nextTick = (function () {
+ var callbacks = [];
+ var pending = false;
+ var timerFunc;
+
+ function nextTickHandler () {
+ pending = false;
+ var copies = callbacks.slice(0);
+ callbacks.length = 0;
+ for (var i = 0; i < copies.length; i++) {
+ copies[i]();
+ }
+ }
+
+ // the nextTick behavior leverages the microtask queue, which can be accessed
+ // via either native Promise.then or MutationObserver.
+ // MutationObserver has wider support, however it is seriously bugged in
+ // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
+ // completely stops working after triggering a few times... so, if native
+ // Promise is available, we will use it:
+ /* istanbul ignore if */
+ if (typeof Promise !== 'undefined' && isNative(Promise)) {
+ var p = Promise.resolve();
+ timerFunc = function () {
+ p.then(nextTickHandler);
+ // in problematic UIWebViews, Promise.then doesn't completely break, but
+ // it can get stuck in a weird state where callbacks are pushed into the
+ // microtask queue but the queue isn't being flushed, until the browser
+ // needs to do some other work, e.g. handle a timer. Therefore we can
+ // "force" the microtask queue to be flushed by adding an empty timer.
+ if (isIOS) { setTimeout(noop); }
+ };
+ } else if (typeof MutationObserver !== 'undefined' && (
+ isNative(MutationObserver) ||
+ // PhantomJS and iOS 7.x
+ MutationObserver.toString() === '[object MutationObserverConstructor]'
+ )) {
+ // use MutationObserver where native Promise is not available,
+ // e.g. PhantomJS IE11, iOS7, Android 4.4
+ var counter = 1;
+ var observer = new MutationObserver(nextTickHandler);
+ var textNode = document.createTextNode(String(counter));
+ observer.observe(textNode, {
+ characterData: true
+ });
+ timerFunc = function () {
+ counter = (counter + 1) % 2;
+ textNode.data = String(counter);
+ };
+ } else {
+ // fallback to setTimeout
+ /* istanbul ignore next */
+ timerFunc = function () {
+ setTimeout(nextTickHandler, 0);
+ };
}
- /**
- * Defer a task to execute it asynchronously. Ideally this
- * should be executed as a microtask, so we leverage
- * MutationObserver if it's available, and fallback to
- * setTimeout(0).
- *
- * @param {Function} cb
- * @param {Object} ctx
- */
-
- var nextTick = (function () {
- var callbacks = [];
- var pending = false;
- var timerFunc;
- function nextTickHandler() {
- pending = false;
- var copies = callbacks.slice(0);
- callbacks = [];
- for (var i = 0; i < copies.length; i++) {
- copies[i]();
- }
- }
-
- /* istanbul ignore if */
- if (typeof MutationObserver !== 'undefined' && !hasMutationObserverBug) {
- var counter = 1;
- var observer = new MutationObserver(nextTickHandler);
- var textNode = document.createTextNode(counter);
- observer.observe(textNode, {
- characterData: true
- });
- timerFunc = function () {
- counter = (counter + 1) % 2;
- textNode.data = counter;
- };
- } else {
- // webpack attempts to inject a shim for setImmediate
- // if it is used as a global, so we have to work around that to
- // avoid bundling unnecessary code.
- var context = inBrowser ? window : typeof global !== 'undefined' ? global : {};
- timerFunc = context.setImmediate || setTimeout;
- }
- return function (cb, ctx) {
- var func = ctx ? function () {
- cb.call(ctx);
- } : cb;
- callbacks.push(func);
- if (pending) return;
+ return function queueNextTick (cb, ctx) {
+ var func = ctx
+ ? function () { cb.call(ctx); }
+ : cb;
+ callbacks.push(func);
+ if (!pending) {
pending = true;
- timerFunc(nextTickHandler, 0);
- };
- })();
+ timerFunc();
+ }
+ }
+})();
- var _Set = undefined;
- /* istanbul ignore if */
- if (typeof Set !== 'undefined' && Set.toString().match(/native code/)) {
- // use native Set when available.
- _Set = Set;
- } else {
- // a non-standard Set polyfill that only works with primitive keys.
- _Set = function () {
+var _Set;
+/* istanbul ignore if */
+if (typeof Set !== 'undefined' && isNative(Set)) {
+ // use native Set when available.
+ _Set = Set;
+} else {
+ // a non-standard Set polyfill that only works with primitive keys.
+ _Set = (function () {
+ function Set () {
this.set = Object.create(null);
+ }
+ Set.prototype.has = function has (key) {
+ return this.set[key] !== undefined
};
- _Set.prototype.has = function (key) {
- return this.set[key] !== undefined;
- };
- _Set.prototype.add = function (key) {
+ Set.prototype.add = function add (key) {
this.set[key] = 1;
};
- _Set.prototype.clear = function () {
+ Set.prototype.clear = function clear () {
this.set = Object.create(null);
};
- }
-
- function Cache(limit) {
- this.size = 0;
- this.limit = limit;
- this.head = this.tail = undefined;
- this._keymap = Object.create(null);
- }
-
- var p = Cache.prototype;
- /**
- * Put <value> into the cache associated with <key>.
- * Returns the entry which was removed to make room for
- * the new entry. Otherwise undefined is returned.
- * (i.e. if there was enough room already).
- *
- * @param {String} key
- * @param {*} value
- * @return {Entry|undefined}
- */
-
- p.put = function (key, value) {
- var removed;
-
- var entry = this.get(key, true);
- if (!entry) {
- if (this.size === this.limit) {
- removed = this.shift();
- }
- entry = {
- key: key
- };
- this._keymap[key] = entry;
- if (this.tail) {
- this.tail.newer = entry;
- entry.older = this.tail;
- } else {
- this.head = entry;
+ return Set;
+ }());
+}
+
+/* not type checking this file because flow doesn't play well with Proxy */
+
+var hasProxy;
+var proxyHandlers;
+var initProxy;
+
+{
+ var allowedGlobals = makeMap(
+ 'Infinity,undefined,NaN,isFinite,isNaN,' +
+ 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
+ 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
+ 'require' // for Webpack/Browserify
+ );
+
+ hasProxy =
+ typeof Proxy !== 'undefined' &&
+ Proxy.toString().match(/native code/);
+
+ proxyHandlers = {
+ has: function has (target, key) {
+ var has = key in target;
+ var isAllowed = allowedGlobals(key) || key.charAt(0) === '_';
+ if (!has && !isAllowed) {
+ warn(
+ "Property or method \"" + key + "\" is not defined on the instance but " +
+ "referenced during render. Make sure to declare reactive data " +
+ "properties in the data option.",
+ target
+ );
}
- this.tail = entry;
- this.size++;
+ return has || !isAllowed
}
- entry.value = value;
-
- return removed;
};
- /**
- * Purge the least recently used (oldest) entry from the
- * cache. Returns the removed entry or undefined if the
- * cache was empty.
- */
-
- p.shift = function () {
- var entry = this.head;
- if (entry) {
- this.head = this.head.newer;
- this.head.older = undefined;
- entry.newer = entry.older = undefined;
- this._keymap[entry.key] = undefined;
- this.size--;
+ initProxy = function initProxy (vm) {
+ if (hasProxy) {
+ vm._renderProxy = new Proxy(vm, proxyHandlers);
+ } else {
+ vm._renderProxy = vm;
}
- return entry;
};
+}
- /**
- * Get and register recent use of <key>. Returns the value
- * associated with <key> or undefined if not in cache.
- *
- * @param {String} key
- * @param {Boolean} returnEntry
- * @return {Entry|*}
- */
+/* */
- p.get = function (key, returnEntry) {
- var entry = this._keymap[key];
- if (entry === undefined) return;
- if (entry === this.tail) {
- return returnEntry ? entry : entry.value;
- }
- // HEAD--------------TAIL
- // <.older .newer>
- // <--- add direction --
- // A B C <D> E
- if (entry.newer) {
- if (entry === this.head) {
- this.head = entry.newer;
- }
- entry.newer.older = entry.older; // C <-- E.
- }
- if (entry.older) {
- entry.older.newer = entry.newer; // C. --> E
- }
- entry.newer = undefined; // D --x
- entry.older = this.tail; // D. --> E
- if (this.tail) {
- this.tail.newer = entry; // E. <-- D
- }
- this.tail = entry;
- return returnEntry ? entry : entry.value;
- };
- var cache$1 = new Cache(1000);
- var filterTokenRE = /[^\s'"]+|'[^']*'|"[^"]*"/g;
- var reservedArgRE = /^in$|^-?\d+/;
+var uid$2 = 0;
- /**
- * Parser state
- */
-
- var str;
- var dir;
- var c;
- var prev;
- var i;
- var l;
- var lastFilterIndex;
- var inSingle;
- var inDouble;
- var curly;
- var square;
- var paren;
- /**
- * Push a filter to the current directive object
- */
+/**
+ * A dep is an observable that can have multiple
+ * directives subscribing to it.
+ */
+var Dep = function Dep () {
+ this.id = uid$2++;
+ this.subs = [];
+};
- function pushFilter() {
- var exp = str.slice(lastFilterIndex, i).trim();
- var filter;
- if (exp) {
- filter = {};
- var tokens = exp.match(filterTokenRE);
- filter.name = tokens[0];
- if (tokens.length > 1) {
- filter.args = tokens.slice(1).map(processFilterArg);
- }
- }
- if (filter) {
- (dir.filters = dir.filters || []).push(filter);
- }
- lastFilterIndex = i + 1;
- }
+Dep.prototype.addSub = function addSub (sub) {
+ this.subs.push(sub);
+};
- /**
- * Check if an argument is dynamic and strip quotes.
- *
- * @param {String} arg
- * @return {Object}
- */
+Dep.prototype.removeSub = function removeSub (sub) {
+ remove$1(this.subs, sub);
+};
- function processFilterArg(arg) {
- if (reservedArgRE.test(arg)) {
- return {
- value: toNumber(arg),
- dynamic: false
- };
- } else {
- var stripped = stripQuotes(arg);
- var dynamic = stripped === arg;
- return {
- value: dynamic ? arg : stripped,
- dynamic: dynamic
- };
- }
+Dep.prototype.depend = function depend () {
+ if (Dep.target) {
+ Dep.target.addDep(this);
}
+};
- /**
- * Parse a directive value and extract the expression
- * and its filters into a descriptor.
- *
- * Example:
- *
- * "a + 1 | uppercase" will yield:
- * {
- * expression: 'a + 1',
- * filters: [
- * { name: 'uppercase', args: null }
- * ]
- * }
- *
- * @param {String} s
- * @return {Object}
- */
+Dep.prototype.notify = function notify () {
+ // stablize the subscriber list first
+ var subs = this.subs.slice();
+ for (var i = 0, l = subs.length; i < l; i++) {
+ subs[i].update();
+ }
+};
- function parseDirective(s) {
- var hit = cache$1.get(s);
- if (hit) {
- return hit;
- }
-
- // reset parser state
- str = s;
- inSingle = inDouble = false;
- curly = square = paren = 0;
- lastFilterIndex = 0;
- dir = {};
-
- for (i = 0, l = str.length; i < l; i++) {
- prev = c;
- c = str.charCodeAt(i);
- if (inSingle) {
- // check single quote
- if (c === 0x27 && prev !== 0x5C) inSingle = !inSingle;
- } else if (inDouble) {
- // check double quote
- if (c === 0x22 && prev !== 0x5C) inDouble = !inDouble;
- } else if (c === 0x7C && // pipe
- str.charCodeAt(i + 1) !== 0x7C && str.charCodeAt(i - 1) !== 0x7C) {
- if (dir.expression == null) {
- // first filter, end of expression
- lastFilterIndex = i + 1;
- dir.expression = str.slice(0, i).trim();
- } else {
- // already has filter
- pushFilter();
- }
- } else {
- switch (c) {
- case 0x22:
- inDouble = true;break; // "
- case 0x27:
- inSingle = true;break; // '
- case 0x28:
- paren++;break; // (
- case 0x29:
- paren--;break; // )
- case 0x5B:
- square++;break; // [
- case 0x5D:
- square--;break; // ]
- case 0x7B:
- curly++;break; // {
- case 0x7D:
- curly--;break; // }
- }
- }
- }
+// the current target watcher being evaluated.
+// this is globally unique because there could be only one
+// watcher being evaluated at any time.
+Dep.target = null;
+var targetStack = [];
- if (dir.expression == null) {
- dir.expression = str.slice(0, i).trim();
- } else if (lastFilterIndex !== 0) {
- pushFilter();
- }
+function pushTarget (_target) {
+ if (Dep.target) { targetStack.push(Dep.target); }
+ Dep.target = _target;
+}
- cache$1.put(s, dir);
- return dir;
- }
+function popTarget () {
+ Dep.target = targetStack.pop();
+}
-var directive = Object.freeze({
- parseDirective: parseDirective
- });
+/* */
- var regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g;
- var cache = undefined;
- var tagRE = undefined;
- var htmlRE = undefined;
- /**
- * Escape a string so it can be used in a RegExp
- * constructor.
- *
- * @param {String} str
- */
- function escapeRegex(str) {
- return str.replace(regexEscapeRE, '\\$&');
- }
+var queue = [];
+var has$1 = {};
+var circular = {};
+var waiting = false;
+var flushing = false;
+var index = 0;
- function compileRegex() {
- var open = escapeRegex(config.delimiters[0]);
- var close = escapeRegex(config.delimiters[1]);
- var unsafeOpen = escapeRegex(config.unsafeDelimiters[0]);
- var unsafeClose = escapeRegex(config.unsafeDelimiters[1]);
- tagRE = new RegExp(unsafeOpen + '((?:.|\\n)+?)' + unsafeClose + '|' + open + '((?:.|\\n)+?)' + close, 'g');
- htmlRE = new RegExp('^' + unsafeOpen + '((?:.|\\n)+?)' + unsafeClose + '$');
- // reset cache
- cache = new Cache(1000);
+/**
+ * Reset the scheduler's state.
+ */
+function resetSchedulerState () {
+ queue.length = 0;
+ has$1 = {};
+ {
+ circular = {};
}
+ waiting = flushing = false;
+}
- /**
- * Parse a template text string into an array of tokens.
- *
- * @param {String} text
- * @return {Array<Object> | null}
- * - {String} type
- * - {String} value
- * - {Boolean} [html]
- * - {Boolean} [oneTime]
- */
-
- function parseText(text) {
- if (!cache) {
- compileRegex();
- }
- var hit = cache.get(text);
- if (hit) {
- return hit;
- }
- if (!tagRE.test(text)) {
- return null;
- }
- var tokens = [];
- var lastIndex = tagRE.lastIndex = 0;
- var match, index, html, value, first, oneTime;
- /* eslint-disable no-cond-assign */
- while (match = tagRE.exec(text)) {
- /* eslint-enable no-cond-assign */
- index = match.index;
- // push text token
- if (index > lastIndex) {
- tokens.push({
- value: text.slice(lastIndex, index)
- });
+/**
+ * Flush both queues and run the watchers.
+ */
+function flushSchedulerQueue () {
+ flushing = true;
+
+ // Sort queue before flush.
+ // This ensures that:
+ // 1. Components are updated from parent to child. (because parent is always
+ // created before the child)
+ // 2. A component's user watchers are run before its render watcher (because
+ // user watchers are created before the render watcher)
+ // 3. If a component is destroyed during a parent component's watcher run,
+ // its watchers can be skipped.
+ queue.sort(function (a, b) { return a.id - b.id; });
+
+ // do not cache length because more watchers might be pushed
+ // as we run existing watchers
+ for (index = 0; index < queue.length; index++) {
+ var watcher = queue[index];
+ var id = watcher.id;
+ has$1[id] = null;
+ watcher.run();
+ // in dev build, check and stop circular updates.
+ if ("development" !== 'production' && has$1[id] != null) {
+ circular[id] = (circular[id] || 0) + 1;
+ if (circular[id] > config._maxUpdateCount) {
+ warn(
+ 'You may have an infinite update loop ' + (
+ watcher.user
+ ? ("in watcher with expression \"" + (watcher.expression) + "\"")
+ : "in a component render function."
+ ),
+ watcher.vm
+ );
+ break
}
- // tag token
- html = htmlRE.test(match[0]);
- value = html ? match[1] : match[2];
- first = value.charCodeAt(0);
- oneTime = first === 42; // *
- value = oneTime ? value.slice(1) : value;
- tokens.push({
- tag: true,
- value: value.trim(),
- html: html,
- oneTime: oneTime
- });
- lastIndex = index + match[0].length;
- }
- if (lastIndex < text.length) {
- tokens.push({
- value: text.slice(lastIndex)
- });
- }
- cache.put(text, tokens);
- return tokens;
- }
-
- /**
- * Format a list of tokens into an expression.
- * e.g. tokens parsed from 'a {{b}} c' can be serialized
- * into one single expression as '"a " + b + " c"'.
- *
- * @param {Array} tokens
- * @param {Vue} [vm]
- * @return {String}
- */
-
- function tokensToExp(tokens, vm) {
- if (tokens.length > 1) {
- return tokens.map(function (token) {
- return formatToken(token, vm);
- }).join('+');
- } else {
- return formatToken(tokens[0], vm, true);
}
}
- /**
- * Format a single token.
- *
- * @param {Object} token
- * @param {Vue} [vm]
- * @param {Boolean} [single]
- * @return {String}
- */
-
- function formatToken(token, vm, single) {
- return token.tag ? token.oneTime && vm ? '"' + vm.$eval(token.value) + '"' : inlineFilters(token.value, single) : '"' + token.value + '"';
+ // devtool hook
+ /* istanbul ignore if */
+ if (devtools && config.devtools) {
+ devtools.emit('flush');
}
- /**
- * For an attribute with multiple interpolation tags,
- * e.g. attr="some-{{thing | filter}}", in order to combine
- * the whole thing into a single watchable expression, we
- * have to inline those filters. This function does exactly
- * that. This is a bit hacky but it avoids heavy changes
- * to directive parser and watcher mechanism.
- *
- * @param {String} exp
- * @param {Boolean} single
- * @return {String}
- */
+ resetSchedulerState();
+}
- var filterRE = /[^|]\|[^|]/;
- function inlineFilters(exp, single) {
- if (!filterRE.test(exp)) {
- return single ? exp : '(' + exp + ')';
+/**
+ * Push a watcher into the watcher queue.
+ * Jobs with duplicate IDs will be skipped unless it's
+ * pushed when the queue is being flushed.
+ */
+function queueWatcher (watcher) {
+ var id = watcher.id;
+ if (has$1[id] == null) {
+ has$1[id] = true;
+ if (!flushing) {
+ queue.push(watcher);
} else {
- var dir = parseDirective(exp);
- if (!dir.filters) {
- return '(' + exp + ')';
- } else {
- return 'this._applyFilters(' + dir.expression + // value
- ',null,' + // oldValue (null for read)
- JSON.stringify(dir.filters) + // filter descriptors
- ',false)'; // write?
+ // if already flushing, splice the watcher based on its id
+ // if already past its id, it will be run next immediately.
+ var i = queue.length - 1;
+ while (i >= 0 && queue[i].id > watcher.id) {
+ i--;
}
+ queue.splice(Math.max(i, index) + 1, 0, watcher);
}
- }
-
-var text = Object.freeze({
- compileRegex: compileRegex,
- parseText: parseText,
- tokensToExp: tokensToExp
- });
-
- var delimiters = ['{{', '}}'];
- var unsafeDelimiters = ['{{{', '}}}'];
-
- var config = Object.defineProperties({
-
- /**
- * Whether to print debug messages.
- * Also enables stack trace for warnings.
- *
- * @type {Boolean}
- */
-
- debug: false,
-
- /**
- * Whether to suppress warnings.
- *
- * @type {Boolean}
- */
-
- silent: false,
-
- /**
- * Whether to use async rendering.
- */
-
- async: true,
-
- /**
- * Whether to warn against errors caught when evaluating
- * expressions.
- */
-
- warnExpressionErrors: true,
-
- /**
- * Whether to allow devtools inspection.
- * Disabled by default in production builds.
- */
-
- devtools: 'development' !== 'production',
-
- /**
- * Internal flag to indicate the delimiters have been
- * changed.
- *
- * @type {Boolean}
- */
-
- _delimitersChanged: true,
-
- /**
- * List of asset types that a component can own.
- *
- * @type {Array}
- */
-
- _assetTypes: ['component', 'directive', 'elementDirective', 'filter', 'transition', 'partial'],
-
- /**
- * prop binding modes
- */
-
- _propBindingModes: {
- ONE_WAY: 0,
- TWO_WAY: 1,
- ONE_TIME: 2
- },
-
- /**
- * Max circular updates allowed in a batcher flush cycle.
- */
-
- _maxUpdateCount: 100
-
- }, {
- delimiters: { /**
- * Interpolation delimiters. Changing these would trigger
- * the text parser to re-compile the regular expressions.
- *
- * @type {Array<String>}
- */
-
- get: function get() {
- return delimiters;
- },
- set: function set(val) {
- delimiters = val;
- compileRegex();
- },
- configurable: true,
- enumerable: true
- },
- unsafeDelimiters: {
- get: function get() {
- return unsafeDelimiters;
- },
- set: function set(val) {
- unsafeDelimiters = val;
- compileRegex();
- },
- configurable: true,
- enumerable: true
+ // queue the flush
+ if (!waiting) {
+ waiting = true;
+ nextTick(flushSchedulerQueue);
}
- });
-
- var warn = undefined;
- var formatComponentName = undefined;
-
- if ('development' !== 'production') {
- (function () {
- var hasConsole = typeof console !== 'undefined';
-
- warn = function (msg, vm) {
- if (hasConsole && !config.silent) {
- console.error('[Vue warn]: ' + msg + (vm ? formatComponentName(vm) : ''));
- }
- };
-
- formatComponentName = function (vm) {
- var name = vm._isVue ? vm.$options.name : vm.name;
- return name ? ' (found in component: <' + hyphenate(name) + '>)' : '';
- };
- })();
}
+}
- /**
- * Append with transition.
- *
- * @param {Element} el
- * @param {Element} target
- * @param {Vue} vm
- * @param {Function} [cb]
- */
-
- function appendWithTransition(el, target, vm, cb) {
- applyTransition(el, 1, function () {
- target.appendChild(el);
- }, vm, cb);
- }
+/* */
- /**
- * InsertBefore with transition.
- *
- * @param {Element} el
- * @param {Element} target
- * @param {Vue} vm
- * @param {Function} [cb]
- */
+var uid$1 = 0;
- function beforeWithTransition(el, target, vm, cb) {
- applyTransition(el, 1, function () {
- before(el, target);
- }, vm, cb);
+/**
+ * A watcher parses an expression, collects dependencies,
+ * and fires callback when the expression value changes.
+ * This is used for both the $watch() api and directives.
+ */
+var Watcher = function Watcher (
+ vm,
+ expOrFn,
+ cb,
+ options
+) {
+ if ( options === void 0 ) options = {};
+
+ this.vm = vm;
+ vm._watchers.push(this);
+ // options
+ this.deep = !!options.deep;
+ this.user = !!options.user;
+ this.lazy = !!options.lazy;
+ this.sync = !!options.sync;
+ this.expression = expOrFn.toString();
+ this.cb = cb;
+ this.id = ++uid$1; // uid for batching
+ this.active = true;
+ this.dirty = this.lazy; // for lazy watchers
+ this.deps = [];
+ this.newDeps = [];
+ this.depIds = new _Set();
+ this.newDepIds = new _Set();
+ // parse expression for getter
+ if (typeof expOrFn === 'function') {
+ this.getter = expOrFn;
+ } else {
+ this.getter = parsePath(expOrFn);
+ if (!this.getter) {
+ this.getter = function () {};
+ "development" !== 'production' && warn(
+ "Failed watching path: \"" + expOrFn + "\" " +
+ 'Watcher only accepts simple dot-delimited paths. ' +
+ 'For full control, use a function instead.',
+ vm
+ );
+ }
+ }
+ this.value = this.lazy
+ ? undefined
+ : this.get();
+};
+
+/**
+ * Evaluate the getter, and re-collect dependencies.
+ */
+Watcher.prototype.get = function get () {
+ pushTarget(this);
+ var value = this.getter.call(this.vm, this.vm);
+ // "touch" every property so they are all tracked as
+ // dependencies for deep watching
+ if (this.deep) {
+ traverse(value);
+ }
+ popTarget();
+ this.cleanupDeps();
+ return value
+};
+
+/**
+ * Add a dependency to this directive.
+ */
+Watcher.prototype.addDep = function addDep (dep) {
+ var id = dep.id;
+ if (!this.newDepIds.has(id)) {
+ this.newDepIds.add(id);
+ this.newDeps.push(dep);
+ if (!this.depIds.has(id)) {
+ dep.addSub(this);
+ }
}
+};
- /**
- * Remove with transition.
- *
- * @param {Element} el
- * @param {Vue} vm
- * @param {Function} [cb]
- */
-
- function removeWithTransition(el, vm, cb) {
- applyTransition(el, -1, function () {
- remove(el);
- }, vm, cb);
+/**
+ * Clean up for dependency collection.
+ */
+Watcher.prototype.cleanupDeps = function cleanupDeps () {
+ var this$1 = this;
+
+ var i = this.deps.length;
+ while (i--) {
+ var dep = this$1.deps[i];
+ if (!this$1.newDepIds.has(dep.id)) {
+ dep.removeSub(this$1);
+ }
+ }
+ var tmp = this.depIds;
+ this.depIds = this.newDepIds;
+ this.newDepIds = tmp;
+ this.newDepIds.clear();
+ tmp = this.deps;
+ this.deps = this.newDeps;
+ this.newDeps = tmp;
+ this.newDeps.length = 0;
+};
+
+/**
+ * Subscriber interface.
+ * Will be called when a dependency changes.
+ */
+Watcher.prototype.update = function update () {
+ /* istanbul ignore else */
+ if (this.lazy) {
+ this.dirty = true;
+ } else if (this.sync) {
+ this.run();
+ } else {
+ queueWatcher(this);
}
+};
- /**
- * Apply transitions with an operation callback.
- *
- * @param {Element} el
- * @param {Number} direction
- * 1: enter
- * -1: leave
- * @param {Function} op - the actual DOM operation
- * @param {Vue} vm
- * @param {Function} [cb]
- */
-
- function applyTransition(el, direction, op, vm, cb) {
- var transition = el.__v_trans;
- if (!transition ||
- // skip if there are no js hooks and CSS transition is
- // not supported
- !transition.hooks && !transitionEndEvent ||
- // skip transitions for initial compile
- !vm._isCompiled ||
- // if the vm is being manipulated by a parent directive
- // during the parent's compilation phase, skip the
- // animation.
- vm.$parent && !vm.$parent._isCompiled) {
- op();
- if (cb) cb();
- return;
- }
- var action = direction > 0 ? 'enter' : 'leave';
- transition[action](op, cb);
- }
-
-var transition = Object.freeze({
- appendWithTransition: appendWithTransition,
- beforeWithTransition: beforeWithTransition,
- removeWithTransition: removeWithTransition,
- applyTransition: applyTransition
- });
-
- /**
- * Query an element selector if it's not an element already.
- *
- * @param {String|Element} el
- * @return {Element}
- */
-
- function query(el) {
- if (typeof el === 'string') {
- var selector = el;
- el = document.querySelector(el);
- if (!el) {
- 'development' !== 'production' && warn('Cannot find element: ' + selector);
+/**
+ * Scheduler job interface.
+ * Will be called by the scheduler.
+ */
+Watcher.prototype.run = function run () {
+ if (this.active) {
+ var value = this.get();
+ if (
+ value !== this.value ||
+ // Deep watchers and watchers on Object/Arrays should fire even
+ // when the value is the same, because the value may
+ // have mutated.
+ isObject(value) ||
+ this.deep
+ ) {
+ // set new value
+ var oldValue = this.value;
+ this.value = value;
+ if (this.user) {
+ try {
+ this.cb.call(this.vm, value, oldValue);
+ } catch (e) {
+ "development" !== 'production' && warn(
+ ("Error in watcher \"" + (this.expression) + "\""),
+ this.vm
+ );
+ /* istanbul ignore else */
+ if (config.errorHandler) {
+ config.errorHandler.call(null, e, this.vm);
+ } else {
+ throw e
+ }
+ }
+ } else {
+ this.cb.call(this.vm, value, oldValue);
}
}
- return el;
}
+};
- /**
- * Check if a node is in the document.
- * Note: document.documentElement.contains should work here
- * but always returns false for comment nodes in phantomjs,
- * making unit tests difficult. This is fixed by doing the
- * contains() check on the node's parentNode instead of
- * the node itself.
- *
- * @param {Node} node
- * @return {Boolean}
- */
-
- function inDoc(node) {
- if (!node) return false;
- var doc = node.ownerDocument.documentElement;
- var parent = node.parentNode;
- return doc === node || doc === parent || !!(parent && parent.nodeType === 1 && doc.contains(parent));
- }
+/**
+ * Evaluate the value of the watcher.
+ * This only gets called for lazy watchers.
+ */
+Watcher.prototype.evaluate = function evaluate () {
+ this.value = this.get();
+ this.dirty = false;
+};
- /**
- * Get and remove an attribute from a node.
- *
- * @param {Node} node
- * @param {String} _attr
- */
+/**
+ * Depend on all deps collected by this watcher.
+ */
+Watcher.prototype.depend = function depend () {
+ var this$1 = this;
- function getAttr(node, _attr) {
- var val = node.getAttribute(_attr);
- if (val !== null) {
- node.removeAttribute(_attr);
- }
- return val;
+ var i = this.deps.length;
+ while (i--) {
+ this$1.deps[i].depend();
}
+};
- /**
- * Get an attribute with colon or v-bind: prefix.
- *
- * @param {Node} node
- * @param {String} name
- * @return {String|null}
- */
+/**
+ * Remove self from all dependencies' subcriber list.
+ */
+Watcher.prototype.teardown = function teardown () {
+ var this$1 = this;
- function getBindAttr(node, name) {
- var val = getAttr(node, ':' + name);
- if (val === null) {
- val = getAttr(node, 'v-bind:' + name);
+ if (this.active) {
+ // remove self from vm's watcher list
+ // this is a somewhat expensive operation so we skip it
+ // if the vm is being destroyed or is performing a v-for
+ // re-render (the watcher list is then filtered by v-for).
+ if (!this.vm._isBeingDestroyed && !this.vm._vForRemoving) {
+ remove$1(this.vm._watchers, this);
}
- return val;
- }
-
- /**
- * Check the presence of a bind attribute.
- *
- * @param {Node} node
- * @param {String} name
- * @return {Boolean}
- */
-
- function hasBindAttr(node, name) {
- return node.hasAttribute(name) || node.hasAttribute(':' + name) || node.hasAttribute('v-bind:' + name);
- }
-
- /**
- * Insert el before target
- *
- * @param {Element} el
- * @param {Element} target
- */
-
- function before(el, target) {
- target.parentNode.insertBefore(el, target);
- }
-
- /**
- * Insert el after target
- *
- * @param {Element} el
- * @param {Element} target
- */
-
- function after(el, target) {
- if (target.nextSibling) {
- before(el, target.nextSibling);
- } else {
- target.parentNode.appendChild(el);
+ var i = this.deps.length;
+ while (i--) {
+ this$1.deps[i].removeSub(this$1);
}
+ this.active = false;
}
+};
- /**
- * Remove el from DOM
- *
- * @param {Element} el
- */
-
- function remove(el) {
- el.parentNode.removeChild(el);
- }
-
- /**
- * Prepend el to target
- *
- * @param {Element} el
- * @param {Element} target
- */
-
- function prepend(el, target) {
- if (target.firstChild) {
- before(el, target.firstChild);
- } else {
- target.appendChild(el);
+/**
+ * Recursively traverse an object to evoke all converted
+ * getters, so that every nested property inside the object
+ * is collected as a "deep" dependency.
+ */
+var seenObjects = new _Set();
+function traverse (val, seen) {
+ var i, keys;
+ if (!seen) {
+ seen = seenObjects;
+ seen.clear();
+ }
+ var isA = Array.isArray(val);
+ var isO = isObject(val);
+ if ((isA || isO) && Object.isExtensible(val)) {
+ if (val.__ob__) {
+ var depId = val.__ob__.dep.id;
+ if (seen.has(depId)) {
+ return
+ } else {
+ seen.add(depId);
+ }
}
- }
-
- /**
- * Replace target with el
- *
- * @param {Element} target
- * @param {Element} el
- */
-
- function replace(target, el) {
- var parent = target.parentNode;
- if (parent) {
- parent.replaceChild(el, target);
+ if (isA) {
+ i = val.length;
+ while (i--) { traverse(val[i], seen); }
+ } else if (isO) {
+ keys = Object.keys(val);
+ i = keys.length;
+ while (i--) { traverse(val[keys[i]], seen); }
}
}
+}
- /**
- * Add event listener shorthand.
- *
- * @param {Element} el
- * @param {String} event
- * @param {Function} cb
- * @param {Boolean} [useCapture]
- */
+/*
+ * not type checking this file because flow doesn't play well with
+ * dynamically accessing methods on Array prototype
+ */
- function on(el, event, cb, useCapture) {
- el.addEventListener(event, cb, useCapture);
- }
+var arrayProto = Array.prototype;
+var arrayMethods = Object.create(arrayProto);[
+ 'push',
+ 'pop',
+ 'shift',
+ 'unshift',
+ 'splice',
+ 'sort',
+ 'reverse'
+]
+.forEach(function (method) {
+ // cache original method
+ var original = arrayProto[method];
+ def(arrayMethods, method, function mutator () {
+ var arguments$1 = arguments;
+
+ // avoid leaking arguments:
+ // http://jsperf.com/closure-with-arguments
+ var i = arguments.length;
+ var args = new Array(i);
+ while (i--) {
+ args[i] = arguments$1[i];
+ }
+ var result = original.apply(this, args);
+ var ob = this.__ob__;
+ var inserted;
+ switch (method) {
+ case 'push':
+ inserted = args;
+ break
+ case 'unshift':
+ inserted = args;
+ break
+ case 'splice':
+ inserted = args.slice(2);
+ break
+ }
+ if (inserted) { ob.observeArray(inserted); }
+ // notify change
+ ob.dep.notify();
+ return result
+ });
+});
- /**
- * Remove event listener shorthand.
- *
- * @param {Element} el
- * @param {String} event
- * @param {Function} cb
- */
+/* */
+
+var arrayKeys = Object.getOwnPropertyNames(arrayMethods);
- function off(el, event, cb) {
- el.removeEventListener(event, cb);
+/**
+ * By default, when a reactive property is set, the new value is
+ * also converted to become reactive. However when passing down props,
+ * we don't want to force conversion because the value may be a nested value
+ * under a frozen data structure. Converting it would defeat the optimization.
+ */
+var observerState = {
+ shouldConvert: true,
+ isSettingProps: false
+};
+
+/**
+ * Observer class that are attached to each observed
+ * object. Once attached, the observer converts target
+ * object's property keys into getter/setters that
+ * collect dependencies and dispatches updates.
+ */
+var Observer = function Observer (value) {
+ this.value = value;
+ this.dep = new Dep();
+ this.vmCount = 0;
+ def(value, '__ob__', this);
+ if (Array.isArray(value)) {
+ var augment = hasProto
+ ? protoAugment
+ : copyAugment;
+ augment(value, arrayMethods, arrayKeys);
+ this.observeArray(value);
+ } else {
+ this.walk(value);
}
+};
- /**
- * For IE9 compat: when both class and :class are present
- * getAttribute('class') returns wrong value...
- *
- * @param {Element} el
- * @return {String}
- */
+/**
+ * Walk through each property and convert them into
+ * getter/setters. This method should only be called when
+ * value type is Object.
+ */
+Observer.prototype.walk = function walk (obj) {
+ var keys = Object.keys(obj);
+ for (var i = 0; i < keys.length; i++) {
+ defineReactive$$1(obj, keys[i], obj[keys[i]]);
+ }
+};
- function getClass(el) {
- var classname = el.className;
- if (typeof classname === 'object') {
- classname = classname.baseVal || '';
- }
- return classname;
+/**
+ * Observe a list of Array items.
+ */
+Observer.prototype.observeArray = function observeArray (items) {
+ for (var i = 0, l = items.length; i < l; i++) {
+ observe(items[i]);
}
+};
- /**
- * In IE9, setAttribute('class') will result in empty class
- * if the element also has the :class attribute; However in
- * PhantomJS, setting `className` does not work on SVG elements...
- * So we have to do a conditional check here.
- *
- * @param {Element} el
- * @param {String} cls
- */
+// helpers
- function setClass(el, cls) {
- /* istanbul ignore if */
- if (isIE9 && !/svg$/.test(el.namespaceURI)) {
- el.className = cls;
- } else {
- el.setAttribute('class', cls);
- }
+/**
+ * Augment an target Object or Array by intercepting
+ * the prototype chain using __proto__
+ */
+function protoAugment (target, src) {
+ /* eslint-disable no-proto */
+ target.__proto__ = src;
+ /* eslint-enable no-proto */
+}
+
+/**
+ * Augment an target Object or Array by defining
+ * hidden properties.
+ *
+ * istanbul ignore next
+ */
+function copyAugment (target, src, keys) {
+ for (var i = 0, l = keys.length; i < l; i++) {
+ var key = keys[i];
+ def(target, key, src[key]);
}
+}
- /**
- * Add class with compatibility for IE & SVG
- *
- * @param {Element} el
- * @param {String} cls
- */
-
- function addClass(el, cls) {
- if (el.classList) {
- el.classList.add(cls);
- } else {
- var cur = ' ' + getClass(el) + ' ';
- if (cur.indexOf(' ' + cls + ' ') < 0) {
- setClass(el, (cur + cls).trim());
+/**
+ * Attempt to create an observer instance for a value,
+ * returns the new observer if successfully observed,
+ * or the existing observer if the value already has one.
+ */
+function observe (value) {
+ if (!isObject(value)) {
+ return
+ }
+ var ob;
+ if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
+ ob = value.__ob__;
+ } else if (
+ observerState.shouldConvert &&
+ !config._isServer &&
+ (Array.isArray(value) || isPlainObject(value)) &&
+ Object.isExtensible(value) &&
+ !value._isVue
+ ) {
+ ob = new Observer(value);
+ }
+ return ob
+}
+
+/**
+ * Define a reactive property on an Object.
+ */
+function defineReactive$$1 (
+ obj,
+ key,
+ val,
+ customSetter
+) {
+ var dep = new Dep();
+
+ var property = Object.getOwnPropertyDescriptor(obj, key);
+ if (property && property.configurable === false) {
+ return
+ }
+
+ // cater for pre-defined getter/setters
+ var getter = property && property.get;
+ var setter = property && property.set;
+
+ var childOb = observe(val);
+ Object.defineProperty(obj, key, {
+ enumerable: true,
+ configurable: true,
+ get: function reactiveGetter () {
+ var value = getter ? getter.call(obj) : val;
+ if (Dep.target) {
+ dep.depend();
+ if (childOb) {
+ childOb.dep.depend();
+ }
+ if (Array.isArray(value)) {
+ dependArray(value);
+ }
+ }
+ return value
+ },
+ set: function reactiveSetter (newVal) {
+ var value = getter ? getter.call(obj) : val;
+ if (newVal === value) {
+ return
+ }
+ if ("development" !== 'production' && customSetter) {
+ customSetter();
+ }
+ if (setter) {
+ setter.call(obj, newVal);
+ } else {
+ val = newVal;
}
+ childOb = observe(newVal);
+ dep.notify();
}
- }
+ });
+}
- /**
- * Remove class with compatibility for IE & SVG
- *
- * @param {Element} el
- * @param {String} cls
- */
+/**
+ * Set a property on an object. Adds the new property and
+ * triggers change notification if the property doesn't
+ * already exist.
+ */
+function set (obj, key, val) {
+ if (Array.isArray(obj)) {
+ obj.splice(key, 1, val);
+ return val
+ }
+ if (hasOwn(obj, key)) {
+ obj[key] = val;
+ return
+ }
+ var ob = obj.__ob__;
+ if (obj._isVue || (ob && ob.vmCount)) {
+ "development" !== 'production' && warn(
+ 'Avoid adding reactive properties to a Vue instance or its root $data ' +
+ 'at runtime - declare it upfront in the data option.'
+ );
+ return
+ }
+ if (!ob) {
+ obj[key] = val;
+ return
+ }
+ defineReactive$$1(ob.value, key, val);
+ ob.dep.notify();
+ return val
+}
+
+/**
+ * Delete a property and trigger change if necessary.
+ */
+function del (obj, key) {
+ var ob = obj.__ob__;
+ if (obj._isVue || (ob && ob.vmCount)) {
+ "development" !== 'production' && warn(
+ 'Avoid deleting properties on a Vue instance or its root $data ' +
+ '- just set it to null.'
+ );
+ return
+ }
+ if (!hasOwn(obj, key)) {
+ return
+ }
+ delete obj[key];
+ if (!ob) {
+ return
+ }
+ ob.dep.notify();
+}
+
+/**
+ * Collect dependencies on array elements when the array is touched, since
+ * we cannot intercept array element access like property getters.
+ */
+function dependArray (value) {
+ for (var e = void 0, i = 0, l = value.length; i < l; i++) {
+ e = value[i];
+ e && e.__ob__ && e.__ob__.dep.depend();
+ if (Array.isArray(e)) {
+ dependArray(e);
+ }
+ }
+}
+
+/* */
+
+function initState (vm) {
+ vm._watchers = [];
+ initProps(vm);
+ initData(vm);
+ initComputed(vm);
+ initMethods(vm);
+ initWatch(vm);
+}
+
+function initProps (vm) {
+ var props = vm.$options.props;
+ if (props) {
+ var propsData = vm.$options.propsData || {};
+ var keys = vm.$options._propKeys = Object.keys(props);
+ var isRoot = !vm.$parent;
+ // root instance props should be converted
+ observerState.shouldConvert = isRoot;
+ var loop = function ( i ) {
+ var key = keys[i];
+ /* istanbul ignore else */
+ {
+ defineReactive$$1(vm, key, validateProp(key, props, propsData, vm), function () {
+ if (vm.$parent && !observerState.isSettingProps) {
+ warn(
+ "Avoid mutating a prop directly since the value will be " +
+ "overwritten whenever the parent component re-renders. " +
+ "Instead, use a data or computed property based on the prop's " +
+ "value. Prop being mutated: \"" + key + "\"",
+ vm
+ );
+ }
+ });
+ }
+ };
- function removeClass(el, cls) {
- if (el.classList) {
- el.classList.remove(cls);
+ for (var i = 0; i < keys.length; i++) loop( i );
+ observerState.shouldConvert = true;
+ }
+}
+
+function initData (vm) {
+ var data = vm.$options.data;
+ data = vm._data = typeof data === 'function'
+ ? data.call(vm)
+ : data || {};
+ if (!isPlainObject(data)) {
+ data = {};
+ "development" !== 'production' && warn(
+ 'data functions should return an object.',
+ vm
+ );
+ }
+ // proxy data on instance
+ var keys = Object.keys(data);
+ var props = vm.$options.props;
+ var i = keys.length;
+ while (i--) {
+ if (props && hasOwn(props, keys[i])) {
+ "development" !== 'production' && warn(
+ "The data property \"" + (keys[i]) + "\" is already declared as a prop. " +
+ "Use prop default value instead.",
+ vm
+ );
} else {
- var cur = ' ' + getClass(el) + ' ';
- var tar = ' ' + cls + ' ';
- while (cur.indexOf(tar) >= 0) {
- cur = cur.replace(tar, ' ');
+ proxy(vm, keys[i]);
+ }
+ }
+ // observe data
+ observe(data);
+ data.__ob__ && data.__ob__.vmCount++;
+}
+
+var computedSharedDefinition = {
+ enumerable: true,
+ configurable: true,
+ get: noop,
+ set: noop
+};
+
+function initComputed (vm) {
+ var computed = vm.$options.computed;
+ if (computed) {
+ for (var key in computed) {
+ var userDef = computed[key];
+ if (typeof userDef === 'function') {
+ computedSharedDefinition.get = makeComputedGetter(userDef, vm);
+ computedSharedDefinition.set = noop;
+ } else {
+ computedSharedDefinition.get = userDef.get
+ ? userDef.cache !== false
+ ? makeComputedGetter(userDef.get, vm)
+ : bind$1(userDef.get, vm)
+ : noop;
+ computedSharedDefinition.set = userDef.set
+ ? bind$1(userDef.set, vm)
+ : noop;
}
- setClass(el, cur.trim());
- }
- if (!el.className) {
- el.removeAttribute('class');
+ Object.defineProperty(vm, key, computedSharedDefinition);
}
}
+}
- /**
- * Extract raw content inside an element into a temporary
- * container div
- *
- * @param {Element} el
- * @param {Boolean} asFragment
- * @return {Element|DocumentFragment}
- */
-
- function extractContent(el, asFragment) {
- var child;
- var rawContent;
- /* istanbul ignore if */
- if (isTemplate(el) && isFragment(el.content)) {
- el = el.content;
- }
- if (el.hasChildNodes()) {
- trimNode(el);
- rawContent = asFragment ? document.createDocumentFragment() : document.createElement('div');
- /* eslint-disable no-cond-assign */
- while (child = el.firstChild) {
- /* eslint-enable no-cond-assign */
- rawContent.appendChild(child);
+function makeComputedGetter (getter, owner) {
+ var watcher = new Watcher(owner, getter, noop, {
+ lazy: true
+ });
+ return function computedGetter () {
+ if (watcher.dirty) {
+ watcher.evaluate();
+ }
+ if (Dep.target) {
+ watcher.depend();
+ }
+ return watcher.value
+ }
+}
+
+function initMethods (vm) {
+ var methods = vm.$options.methods;
+ if (methods) {
+ for (var key in methods) {
+ vm[key] = methods[key] == null ? noop : bind$1(methods[key], vm);
+ if ("development" !== 'production' && methods[key] == null) {
+ warn(
+ "method \"" + key + "\" has an undefined value in the component definition. " +
+ "Did you reference the function correctly?",
+ vm
+ );
}
}
- return rawContent;
}
+}
- /**
- * Trim possible empty head/tail text and comment
- * nodes inside a parent.
- *
- * @param {Node} node
- */
-
- function trimNode(node) {
- var child;
- /* eslint-disable no-sequences */
- while ((child = node.firstChild, isTrimmable(child))) {
- node.removeChild(child);
- }
- while ((child = node.lastChild, isTrimmable(child))) {
- node.removeChild(child);
+function initWatch (vm) {
+ var watch = vm.$options.watch;
+ if (watch) {
+ for (var key in watch) {
+ var handler = watch[key];
+ if (Array.isArray(handler)) {
+ for (var i = 0; i < handler.length; i++) {
+ createWatcher(vm, key, handler[i]);
+ }
+ } else {
+ createWatcher(vm, key, handler);
+ }
}
- /* eslint-enable no-sequences */
}
+}
- function isTrimmable(node) {
- return node && (node.nodeType === 3 && !node.data.trim() || node.nodeType === 8);
+function createWatcher (vm, key, handler) {
+ var options;
+ if (isPlainObject(handler)) {
+ options = handler;
+ handler = handler.handler;
}
-
- /**
- * Check if an element is a template tag.
- * Note if the template appears inside an SVG its tagName
- * will be in lowercase.
- *
- * @param {Element} el
- */
-
- function isTemplate(el) {
- return el.tagName && el.tagName.toLowerCase() === 'template';
+ if (typeof handler === 'string') {
+ handler = vm[handler];
}
+ vm.$watch(key, handler, options);
+}
- /**
- * Create an "anchor" for performing dom insertion/removals.
- * This is used in a number of scenarios:
- * - fragment instance
- * - v-html
- * - v-if
- * - v-for
- * - component
- *
- * @param {String} content
- * @param {Boolean} persist - IE trashes empty textNodes on
- * cloneNode(true), so in certain
- * cases the anchor needs to be
- * non-empty to be persisted in
- * templates.
- * @return {Comment|Text}
- */
-
- function createAnchor(content, persist) {
- var anchor = config.debug ? document.createComment(content) : document.createTextNode(persist ? ' ' : '');
- anchor.__v_anchor = true;
- return anchor;
+function stateMixin (Vue) {
+ // flow somehow has problems with directly declared definition object
+ // when using Object.defineProperty, so we have to procedurally build up
+ // the object here.
+ var dataDef = {};
+ dataDef.get = function () {
+ return this._data
+ };
+ {
+ dataDef.set = function (newData) {
+ warn(
+ 'Avoid replacing instance root $data. ' +
+ 'Use nested data properties instead.',
+ this
+ );
+ };
}
+ Object.defineProperty(Vue.prototype, '$data', dataDef);
- /**
- * Find a component ref attribute that starts with $.
- *
- * @param {Element} node
- * @return {String|undefined}
- */
-
- var refRE = /^v-ref:/;
+ Vue.prototype.$set = set;
+ Vue.prototype.$delete = del;
- function findRef(node) {
- if (node.hasAttributes()) {
- var attrs = node.attributes;
- for (var i = 0, l = attrs.length; i < l; i++) {
- var name = attrs[i].name;
- if (refRE.test(name)) {
- return camelize(name.replace(refRE, ''));
- }
- }
+ Vue.prototype.$watch = function (
+ expOrFn,
+ cb,
+ options
+ ) {
+ var vm = this;
+ options = options || {};
+ options.user = true;
+ var watcher = new Watcher(vm, expOrFn, cb, options);
+ if (options.immediate) {
+ cb.call(vm, watcher.value);
}
- }
-
- /**
- * Map a function to a range of nodes .
- *
- * @param {Node} node
- * @param {Node} end
- * @param {Function} op
- */
-
- function mapNodeRange(node, end, op) {
- var next;
- while (node !== end) {
- next = node.nextSibling;
- op(node);
- node = next;
+ return function unwatchFn () {
+ watcher.teardown();
}
- op(end);
- }
-
- /**
- * Remove a range of nodes with transition, store
- * the nodes in a fragment with correct ordering,
- * and call callback when done.
- *
- * @param {Node} start
- * @param {Node} end
- * @param {Vue} vm
- * @param {DocumentFragment} frag
- * @param {Function} cb
- */
+ };
+}
- function removeNodeRange(start, end, vm, frag, cb) {
- var done = false;
- var removed = 0;
- var nodes = [];
- mapNodeRange(start, end, function (node) {
- if (node === end) done = true;
- nodes.push(node);
- removeWithTransition(node, vm, onRemoved);
- });
- function onRemoved() {
- removed++;
- if (done && removed >= nodes.length) {
- for (var i = 0; i < nodes.length; i++) {
- frag.appendChild(nodes[i]);
- }
- cb && cb();
+function proxy (vm, key) {
+ if (!isReserved(key)) {
+ Object.defineProperty(vm, key, {
+ configurable: true,
+ enumerable: true,
+ get: function proxyGetter () {
+ return vm._data[key]
+ },
+ set: function proxySetter (val) {
+ vm._data[key] = val;
}
- }
- }
-
- /**
- * Check if a node is a DocumentFragment.
- *
- * @param {Node} node
- * @return {Boolean}
- */
-
- function isFragment(node) {
- return node && node.nodeType === 11;
+ });
}
-
- /**
- * Get outerHTML of elements, taking care
- * of SVG elements in IE as well.
- *
- * @param {Element} el
- * @return {String}
- */
-
- function getOuterHTML(el) {
- if (el.outerHTML) {
- return el.outerHTML;
+}
+
+/* */
+
+var VNode = function VNode (
+ tag,
+ data,
+ children,
+ text,
+ elm,
+ ns,
+ context,
+ componentOptions
+) {
+ this.tag = tag;
+ this.data = data;
+ this.children = children;
+ this.text = text;
+ this.elm = elm;
+ this.ns = ns;
+ this.context = context;
+ this.functionalContext = undefined;
+ this.key = data && data.key;
+ this.componentOptions = componentOptions;
+ this.child = undefined;
+ this.parent = undefined;
+ this.raw = false;
+ this.isStatic = false;
+ this.isRootInsert = true;
+ this.isComment = false;
+ this.isCloned = false;
+};
+
+var emptyVNode = function () {
+ var node = new VNode();
+ node.text = '';
+ node.isComment = true;
+ return node
+};
+
+// optimized shallow clone
+// used for static nodes and slot nodes because they may be reused across
+// multiple renders, cloning them avoids errors when DOM manipulations rely
+// on their elm reference.
+function cloneVNode (vnode) {
+ var cloned = new VNode(
+ vnode.tag,
+ vnode.data,
+ vnode.children,
+ vnode.text,
+ vnode.elm,
+ vnode.ns,
+ vnode.context,
+ vnode.componentOptions
+ );
+ cloned.isStatic = vnode.isStatic;
+ cloned.key = vnode.key;
+ cloned.isCloned = true;
+ return cloned
+}
+
+function cloneVNodes (vnodes) {
+ var res = new Array(vnodes.length);
+ for (var i = 0; i < vnodes.length; i++) {
+ res[i] = cloneVNode(vnodes[i]);
+ }
+ return res
+}
+
+/* */
+
+function mergeVNodeHook (def, hookKey, hook, key) {
+ key = key + hookKey;
+ var injectedHash = def.__injected || (def.__injected = {});
+ if (!injectedHash[key]) {
+ injectedHash[key] = true;
+ var oldHook = def[hookKey];
+ if (oldHook) {
+ def[hookKey] = function () {
+ oldHook.apply(this, arguments);
+ hook.apply(this, arguments);
+ };
} else {
- var container = document.createElement('div');
- container.appendChild(el.cloneNode(true));
- return container.innerHTML;
- }
- }
-
- var commonTagRE = /^(div|p|span|img|a|b|i|br|ul|ol|li|h1|h2|h3|h4|h5|h6|code|pre|table|th|td|tr|form|label|input|select|option|nav|article|section|header|footer)$/i;
- var reservedTagRE = /^(slot|partial|component)$/i;
-
- var isUnknownElement = undefined;
- if ('development' !== 'production') {
- isUnknownElement = function (el, tag) {
- if (tag.indexOf('-') > -1) {
- // http://stackoverflow.com/a/28210364/1070244
- return el.constructor === window.HTMLUnknownElement || el.constructor === window.HTMLElement;
+ def[hookKey] = hook;
+ }
+ }
+}
+
+/* */
+
+function updateListeners (
+ on,
+ oldOn,
+ add,
+ remove$$1,
+ vm
+) {
+ var name, cur, old, fn, event, capture;
+ for (name in on) {
+ cur = on[name];
+ old = oldOn[name];
+ if (!cur) {
+ "development" !== 'production' && warn(
+ "Invalid handler for event \"" + name + "\": got " + String(cur),
+ vm
+ );
+ } else if (!old) {
+ capture = name.charAt(0) === '!';
+ event = capture ? name.slice(1) : name;
+ if (Array.isArray(cur)) {
+ add(event, (cur.invoker = arrInvoker(cur)), capture);
} else {
- return (/HTMLUnknownElement/.test(el.toString()) &&
- // Chrome returns unknown for several HTML5 elements.
- // https://code.google.com/p/chromium/issues/detail?id=540526
- // Firefox returns unknown for some "Interactive elements."
- !/^(data|time|rtc|rb|details|dialog|summary)$/.test(tag)
- );
- }
- };
- }
-
- /**
- * Check if an element is a component, if yes return its
- * component id.
- *
- * @param {Element} el
- * @param {Object} options
- * @return {Object|undefined}
- */
-
- function checkComponentAttr(el, options) {
- var tag = el.tagName.toLowerCase();
- var hasAttrs = el.hasAttributes();
- if (!commonTagRE.test(tag) && !reservedTagRE.test(tag)) {
- if (resolveAsset(options, 'components', tag)) {
- return { id: tag };
+ if (!cur.invoker) {
+ fn = cur;
+ cur = on[name] = {};
+ cur.fn = fn;
+ cur.invoker = fnInvoker(cur);
+ }
+ add(event, cur.invoker, capture);
+ }
+ } else if (cur !== old) {
+ if (Array.isArray(old)) {
+ old.length = cur.length;
+ for (var i = 0; i < old.length; i++) { old[i] = cur[i]; }
+ on[name] = old;
} else {
- var is = hasAttrs && getIsBinding(el, options);
- if (is) {
- return is;
- } else if ('development' !== 'production') {
- var expectedTag = options._componentNameMap && options._componentNameMap[tag];
- if (expectedTag) {
- warn('Unknown custom element: <' + tag + '> - ' + 'did you mean <' + expectedTag + '>? ' + 'HTML is case-insensitive, remember to use kebab-case in templates.');
- } else if (isUnknownElement(el, tag)) {
- warn('Unknown custom element: <' + tag + '> - did you ' + 'register the component correctly? For recursive components, ' + 'make sure to provide the "name" option.');
- }
- }
+ old.fn = cur;
+ on[name] = old;
}
- } else if (hasAttrs) {
- return getIsBinding(el, options);
}
}
-
- /**
- * Get "is" binding from an element.
- *
- * @param {Element} el
- * @param {Object} options
- * @return {Object|undefined}
- */
-
- function getIsBinding(el, options) {
- // dynamic syntax
- var exp = el.getAttribute('is');
- if (exp != null) {
- if (resolveAsset(options, 'components', exp)) {
- el.removeAttribute('is');
- return { id: exp };
- }
- } else {
- exp = getBindAttr(el, 'is');
- if (exp != null) {
- return { id: exp, dynamic: true };
- }
+ for (name in oldOn) {
+ if (!on[name]) {
+ event = name.charAt(0) === '!' ? name.slice(1) : name;
+ remove$$1(event, oldOn[name].invoker);
}
}
+}
- /**
- * Option overwriting strategies are functions that handle
- * how to merge a parent option value and a child option
- * value into the final value.
- *
- * All strategy functions follow the same signature:
- *
- * @param {*} parentVal
- * @param {*} childVal
- * @param {Vue} [vm]
- */
-
- var strats = config.optionMergeStrategies = Object.create(null);
+function arrInvoker (arr) {
+ return function (ev) {
+ var arguments$1 = arguments;
- /**
- * Helper that recursively merges two data objects together.
- */
-
- function mergeData(to, from) {
- var key, toVal, fromVal;
- for (key in from) {
- toVal = to[key];
- fromVal = from[key];
- if (!hasOwn(to, key)) {
- set(to, key, fromVal);
- } else if (isObject(toVal) && isObject(fromVal)) {
- mergeData(toVal, fromVal);
- }
+ var single = arguments.length === 1;
+ for (var i = 0; i < arr.length; i++) {
+ single ? arr[i](ev) : arr[i].apply(null, arguments$1);
}
- return to;
}
+}
- /**
- * Data
- */
+function fnInvoker (o) {
+ return function (ev) {
+ var single = arguments.length === 1;
+ single ? o.fn(ev) : o.fn.apply(null, arguments);
+ }
+}
- strats.data = function (parentVal, childVal, vm) {
- if (!vm) {
- // in a Vue.extend merge, both should be functions
- if (!childVal) {
- return parentVal;
- }
- if (typeof childVal !== 'function') {
- 'development' !== 'production' && warn('The "data" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm);
- return parentVal;
- }
- if (!parentVal) {
- return childVal;
- }
- // when parentVal & childVal are both present,
- // we need to return a function that returns the
- // merged result of both functions... no need to
- // check if parentVal is a function here because
- // it has to be a function to pass previous merges.
- return function mergedDataFn() {
- return mergeData(childVal.call(this), parentVal.call(this));
- };
- } else if (parentVal || childVal) {
- return function mergedInstanceDataFn() {
- // instance merge
- var instanceData = typeof childVal === 'function' ? childVal.call(vm) : childVal;
- var defaultData = typeof parentVal === 'function' ? parentVal.call(vm) : undefined;
- if (instanceData) {
- return mergeData(instanceData, defaultData);
+/* */
+
+function normalizeChildren (
+ children,
+ ns,
+ nestedIndex
+) {
+ if (isPrimitive(children)) {
+ return [createTextVNode(children)]
+ }
+ if (Array.isArray(children)) {
+ var res = [];
+ for (var i = 0, l = children.length; i < l; i++) {
+ var c = children[i];
+ var last = res[res.length - 1];
+ // nested
+ if (Array.isArray(c)) {
+ res.push.apply(res, normalizeChildren(c, ns, ((nestedIndex || '') + "_" + i)));
+ } else if (isPrimitive(c)) {
+ if (last && last.text) {
+ last.text += String(c);
+ } else if (c !== '') {
+ // convert primitive to vnode
+ res.push(createTextVNode(c));
+ }
+ } else if (c instanceof VNode) {
+ if (c.text && last && last.text) {
+ last.text += c.text;
} else {
- return defaultData;
+ // inherit parent namespace
+ if (ns) {
+ applyNS(c, ns);
+ }
+ // default key for nested array children (likely generated by v-for)
+ if (c.tag && c.key == null && nestedIndex != null) {
+ c.key = "__vlist" + nestedIndex + "_" + i + "__";
+ }
+ res.push(c);
}
- };
- }
- };
-
- /**
- * El
- */
-
- strats.el = function (parentVal, childVal, vm) {
- if (!vm && childVal && typeof childVal !== 'function') {
- 'development' !== 'production' && warn('The "el" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm);
- return;
+ }
}
- var ret = childVal || parentVal;
- // invoke the element factory if this is instance merge
- return vm && typeof ret === 'function' ? ret.call(vm) : ret;
- };
-
- /**
- * Hooks and param attributes are merged as arrays.
- */
-
- strats.init = strats.created = strats.ready = strats.attached = strats.detached = strats.beforeCompile = strats.compiled = strats.beforeDestroy = strats.destroyed = strats.activate = function (parentVal, childVal) {
- return childVal ? parentVal ? parentVal.concat(childVal) : isArray(childVal) ? childVal : [childVal] : parentVal;
- };
-
- /**
- * Assets
- *
- * When a vm is present (instance creation), we need to do
- * a three-way merge between constructor options, instance
- * options and parent options.
- */
-
- function mergeAssets(parentVal, childVal) {
- var res = Object.create(parentVal || null);
- return childVal ? extend(res, guardArrayAssets(childVal)) : res;
+ return res
}
+}
- config._assetTypes.forEach(function (type) {
- strats[type + 's'] = mergeAssets;
- });
-
- /**
- * Events & Watchers.
- *
- * Events & watchers hashes should not overwrite one
- * another, so we merge them as arrays.
- */
+function createTextVNode (val) {
+ return new VNode(undefined, undefined, undefined, String(val))
+}
- strats.watch = strats.events = function (parentVal, childVal) {
- if (!childVal) return parentVal;
- if (!parentVal) return childVal;
- var ret = {};
- extend(ret, parentVal);
- for (var key in childVal) {
- var parent = ret[key];
- var child = childVal[key];
- if (parent && !isArray(parent)) {
- parent = [parent];
+function applyNS (vnode, ns) {
+ if (vnode.tag && !vnode.ns) {
+ vnode.ns = ns;
+ if (vnode.children) {
+ for (var i = 0, l = vnode.children.length; i < l; i++) {
+ applyNS(vnode.children[i], ns);
}
- ret[key] = parent ? parent.concat(child) : [child];
}
- return ret;
- };
+ }
+}
- /**
- * Other object hashes.
- */
+/* */
- strats.props = strats.methods = strats.computed = function (parentVal, childVal) {
- if (!childVal) return parentVal;
- if (!parentVal) return childVal;
- var ret = Object.create(null);
- extend(ret, parentVal);
- extend(ret, childVal);
- return ret;
- };
+function getFirstComponentChild (children) {
+ return children && children.filter(function (c) { return c && c.componentOptions; })[0]
+}
- /**
- * Default strategy.
- */
+/* */
- var defaultStrat = function defaultStrat(parentVal, childVal) {
- return childVal === undefined ? parentVal : childVal;
- };
+var activeInstance = null;
- /**
- * Make sure component options get converted to actual
- * constructors.
- *
- * @param {Object} options
- */
+function initLifecycle (vm) {
+ var options = vm.$options;
- function guardComponents(options) {
- if (options.components) {
- var components = options.components = guardArrayAssets(options.components);
- var ids = Object.keys(components);
- var def;
- if ('development' !== 'production') {
- var map = options._componentNameMap = {};
- }
- for (var i = 0, l = ids.length; i < l; i++) {
- var key = ids[i];
- if (commonTagRE.test(key) || reservedTagRE.test(key)) {
- 'development' !== 'production' && warn('Do not use built-in or reserved HTML elements as component ' + 'id: ' + key);
- continue;
- }
- // record a all lowercase <-> kebab-case mapping for
- // possible custom element case error warning
- if ('development' !== 'production') {
- map[key.replace(/-/g, '').toLowerCase()] = hyphenate(key);
- }
- def = components[key];
- if (isPlainObject(def)) {
- components[key] = Vue.extend(def);
- }
- }
+ // locate first non-abstract parent
+ var parent = options.parent;
+ if (parent && !options.abstract) {
+ while (parent.$options.abstract && parent.$parent) {
+ parent = parent.$parent;
}
+ parent.$children.push(vm);
}
- /**
- * Ensure all props option syntax are normalized into the
- * Object-based format.
- *
- * @param {Object} options
- */
+ vm.$parent = parent;
+ vm.$root = parent ? parent.$root : vm;
- function guardProps(options) {
- var props = options.props;
- var i, val;
- if (isArray(props)) {
- options.props = {};
- i = props.length;
- while (i--) {
- val = props[i];
- if (typeof val === 'string') {
- options.props[val] = null;
- } else if (val.name) {
- options.props[val.name] = val;
- }
- }
- } else if (isPlainObject(props)) {
- var keys = Object.keys(props);
- i = keys.length;
- while (i--) {
- val = props[keys[i]];
- if (typeof val === 'function') {
- props[keys[i]] = { type: val };
- }
- }
- }
- }
+ vm.$children = [];
+ vm.$refs = {};
- /**
- * Guard an Array-format assets option and converted it
- * into the key-value Object format.
- *
- * @param {Object|Array} assets
- * @return {Object}
- */
+ vm._watcher = null;
+ vm._inactive = false;
+ vm._isMounted = false;
+ vm._isDestroyed = false;
+ vm._isBeingDestroyed = false;
+}
- function guardArrayAssets(assets) {
- if (isArray(assets)) {
- var res = {};
- var i = assets.length;
- var asset;
- while (i--) {
- asset = assets[i];
- var id = typeof asset === 'function' ? asset.options && asset.options.name || asset.id : asset.name || asset.id;
- if (!id) {
- 'development' !== 'production' && warn('Array-syntax assets must provide a "name" or "id" field.');
+function lifecycleMixin (Vue) {
+ Vue.prototype._mount = function (
+ el,
+ hydrating
+ ) {
+ var vm = this;
+ vm.$el = el;
+ if (!vm.$options.render) {
+ vm.$options.render = emptyVNode;
+ {
+ /* istanbul ignore if */
+ if (vm.$options.template) {
+ warn(
+ 'You are using the runtime-only build of Vue where the template ' +
+ 'option is not available. Either pre-compile the templates into ' +
+ 'render functions, or use the compiler-included build.',
+ vm
+ );
} else {
- res[id] = asset;
+ warn(
+ 'Failed to mount component: template or render function not defined.',
+ vm
+ );
}
}
- return res;
}
- return assets;
- }
-
- /**
- * Merge two option objects into a new one.
- * Core utility used in both instantiation and inheritance.
- *
- * @param {Object} parent
- * @param {Object} child
- * @param {Vue} [vm] - if vm is present, indicates this is
- * an instantiation merge.
- */
-
- function mergeOptions(parent, child, vm) {
- guardComponents(child);
- guardProps(child);
- if ('development' !== 'production') {
- if (child.propsData && !vm) {
- warn('propsData can only be used as an instantiation option.');
- }
+ callHook(vm, 'beforeMount');
+ vm._watcher = new Watcher(vm, function () {
+ vm._update(vm._render(), hydrating);
+ }, noop);
+ hydrating = false;
+ // manually mounted instance, call mounted on self
+ // mounted is called for render-created child components in its inserted hook
+ if (vm.$vnode == null) {
+ vm._isMounted = true;
+ callHook(vm, 'mounted');
}
- var options = {};
- var key;
- if (child['extends']) {
- parent = typeof child['extends'] === 'function' ? mergeOptions(parent, child['extends'].options, vm) : mergeOptions(parent, child['extends'], vm);
+ return vm
+ };
+
+ Vue.prototype._update = function (vnode, hydrating) {
+ var vm = this;
+ if (vm._isMounted) {
+ callHook(vm, 'beforeUpdate');
+ }
+ var prevEl = vm.$el;
+ var prevActiveInstance = activeInstance;
+ activeInstance = vm;
+ var prevVnode = vm._vnode;
+ vm._vnode = vnode;
+ if (!prevVnode) {
+ // Vue.prototype.__patch__ is injected in entry points
+ // based on the rendering backend used.
+ vm.$el = vm.__patch__(vm.$el, vnode, hydrating);
+ } else {
+ vm.$el = vm.__patch__(prevVnode, vnode);
}
- if (child.mixins) {
- for (var i = 0, l = child.mixins.length; i < l; i++) {
- var mixin = child.mixins[i];
- var mixinOptions = mixin.prototype instanceof Vue ? mixin.options : mixin;
- parent = mergeOptions(parent, mixinOptions, vm);
- }
+ activeInstance = prevActiveInstance;
+ // update __vue__ reference
+ if (prevEl) {
+ prevEl.__vue__ = null;
}
- for (key in parent) {
- mergeField(key);
+ if (vm.$el) {
+ vm.$el.__vue__ = vm;
}
- for (key in child) {
- if (!hasOwn(parent, key)) {
- mergeField(key);
- }
+ // if parent is an HOC, update its $el as well
+ if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
+ vm.$parent.$el = vm.$el;
}
- function mergeField(key) {
- var strat = strats[key] || defaultStrat;
- options[key] = strat(parent[key], child[key], vm, key);
+ if (vm._isMounted) {
+ callHook(vm, 'updated');
}
- return options;
- }
-
- /**
- * Resolve an asset.
- * This function is used because child instances need access
- * to assets defined in its ancestor chain.
- *
- * @param {Object} options
- * @param {String} type
- * @param {String} id
- * @param {Boolean} warnMissing
- * @return {Object|Function}
- */
+ };
- function resolveAsset(options, type, id, warnMissing) {
- /* istanbul ignore if */
- if (typeof id !== 'string') {
- return;
+ Vue.prototype._updateFromParent = function (
+ propsData,
+ listeners,
+ parentVnode,
+ renderChildren
+ ) {
+ var vm = this;
+ var hasChildren = !!(vm.$options._renderChildren || renderChildren);
+ vm.$options._parentVnode = parentVnode;
+ vm.$options._renderChildren = renderChildren;
+ // update props
+ if (propsData && vm.$options.props) {
+ observerState.shouldConvert = false;
+ {
+ observerState.isSettingProps = true;
+ }
+ var propKeys = vm.$options._propKeys || [];
+ for (var i = 0; i < propKeys.length; i++) {
+ var key = propKeys[i];
+ vm[key] = validateProp(key, vm.$options.props, propsData, vm);
+ }
+ observerState.shouldConvert = true;
+ {
+ observerState.isSettingProps = false;
+ }
}
- var assets = options[type];
- var camelizedId;
- var res = assets[id] ||
- // camelCase ID
- assets[camelizedId = camelize(id)] ||
- // Pascal Case ID
- assets[camelizedId.charAt(0).toUpperCase() + camelizedId.slice(1)];
- if ('development' !== 'production' && warnMissing && !res) {
- warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id, options);
+ // update listeners
+ if (listeners) {
+ var oldListeners = vm.$options._parentListeners;
+ vm.$options._parentListeners = listeners;
+ vm._updateListeners(listeners, oldListeners);
}
- return res;
- }
-
- var uid$1 = 0;
-
- /**
- * A dep is an observable that can have multiple
- * directives subscribing to it.
- *
- * @constructor
- */
- function Dep() {
- this.id = uid$1++;
- this.subs = [];
- }
-
- // the current target watcher being evaluated.
- // this is globally unique because there could be only one
- // watcher being evaluated at any time.
- Dep.target = null;
-
- /**
- * Add a directive subscriber.
- *
- * @param {Directive} sub
- */
-
- Dep.prototype.addSub = function (sub) {
- this.subs.push(sub);
- };
-
- /**
- * Remove a directive subscriber.
- *
- * @param {Directive} sub
- */
-
- Dep.prototype.removeSub = function (sub) {
- this.subs.$remove(sub);
- };
-
- /**
- * Add self as a dependency to the target watcher.
- */
-
- Dep.prototype.depend = function () {
- Dep.target.addDep(this);
- };
-
- /**
- * Notify all subscribers of a new value.
- */
-
- Dep.prototype.notify = function () {
- // stablize the subscriber list first
- var subs = toArray(this.subs);
- for (var i = 0, l = subs.length; i < l; i++) {
- subs[i].update();
+ // resolve slots + force update if has children
+ if (hasChildren) {
+ vm.$slots = resolveSlots(renderChildren, vm._renderContext);
+ vm.$forceUpdate();
}
};
- var arrayProto = Array.prototype;
- var arrayMethods = Object.create(arrayProto)
-
- /**
- * Intercept mutating methods and emit events
- */
-
- ;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
- // cache original method
- var original = arrayProto[method];
- def(arrayMethods, method, function mutator() {
- // avoid leaking arguments:
- // http://jsperf.com/closure-with-arguments
- var i = arguments.length;
- var args = new Array(i);
- while (i--) {
- args[i] = arguments[i];
- }
- var result = original.apply(this, args);
- var ob = this.__ob__;
- var inserted;
- switch (method) {
- case 'push':
- inserted = args;
- break;
- case 'unshift':
- inserted = args;
- break;
- case 'splice':
- inserted = args.slice(2);
- break;
- }
- if (inserted) ob.observeArray(inserted);
- // notify change
- ob.dep.notify();
- return result;
- });
- });
-
- /**
- * Swap the element at the given index with a new value
- * and emits corresponding event.
- *
- * @param {Number} index
- * @param {*} val
- * @return {*} - replaced element
- */
-
- def(arrayProto, '$set', function $set(index, val) {
- if (index >= this.length) {
- this.length = Number(index) + 1;
+ Vue.prototype.$forceUpdate = function () {
+ var vm = this;
+ if (vm._watcher) {
+ vm._watcher.update();
}
- return this.splice(index, 1, val)[0];
- });
-
- /**
- * Convenience method to remove the element at given index or target element reference.
- *
- * @param {*} item
- */
+ };
- def(arrayProto, '$remove', function $remove(item) {
- /* istanbul ignore if */
- if (!this.length) return;
- var index = indexOf(this, item);
- if (index > -1) {
- return this.splice(index, 1);
+ Vue.prototype.$destroy = function () {
+ var vm = this;
+ if (vm._isBeingDestroyed) {
+ return
}
- });
-
- var arrayKeys = Object.getOwnPropertyNames(arrayMethods);
-
- /**
- * By default, when a reactive property is set, the new value is
- * also converted to become reactive. However in certain cases, e.g.
- * v-for scope alias and props, we don't want to force conversion
- * because the value may be a nested value under a frozen data structure.
- *
- * So whenever we want to set a reactive property without forcing
- * conversion on the new value, we wrap that call inside this function.
- */
-
- var shouldConvert = true;
-
- function withoutConversion(fn) {
- shouldConvert = false;
- fn();
- shouldConvert = true;
- }
-
- /**
- * Observer class that are attached to each observed
- * object. Once attached, the observer converts target
- * object's property keys into getter/setters that
- * collect dependencies and dispatches updates.
- *
- * @param {Array|Object} value
- * @constructor
- */
-
- function Observer(value) {
- this.value = value;
- this.dep = new Dep();
- def(value, '__ob__', this);
- if (isArray(value)) {
- var augment = hasProto ? protoAugment : copyAugment;
- augment(value, arrayMethods, arrayKeys);
- this.observeArray(value);
- } else {
- this.walk(value);
+ callHook(vm, 'beforeDestroy');
+ vm._isBeingDestroyed = true;
+ // remove self from parent
+ var parent = vm.$parent;
+ if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
+ remove$1(parent.$children, vm);
}
- }
-
- // Instance methods
-
- /**
- * Walk through each property and convert them into
- * getter/setters. This method should only be called when
- * value type is Object.
- *
- * @param {Object} obj
- */
-
- Observer.prototype.walk = function (obj) {
- var keys = Object.keys(obj);
- for (var i = 0, l = keys.length; i < l; i++) {
- this.convert(keys[i], obj[keys[i]]);
+ // teardown watchers
+ if (vm._watcher) {
+ vm._watcher.teardown();
}
+ var i = vm._watchers.length;
+ while (i--) {
+ vm._watchers[i].teardown();
+ }
+ // remove reference from data ob
+ // frozen object may not have observer.
+ if (vm._data.__ob__) {
+ vm._data.__ob__.vmCount--;
+ }
+ // call the last hook...
+ vm._isDestroyed = true;
+ callHook(vm, 'destroyed');
+ // turn off all instance listeners.
+ vm.$off();
+ // remove __vue__ reference
+ if (vm.$el) {
+ vm.$el.__vue__ = null;
+ }
+ // invoke destroy hooks on current rendered tree
+ vm.__patch__(vm._vnode, null);
};
+}
- /**
- * Observe a list of Array items.
- *
- * @param {Array} items
- */
-
- Observer.prototype.observeArray = function (items) {
- for (var i = 0, l = items.length; i < l; i++) {
- observe(items[i]);
+function callHook (vm, hook) {
+ var handlers = vm.$options[hook];
+ if (handlers) {
+ for (var i = 0, j = handlers.length; i < j; i++) {
+ handlers[i].call(vm);
}
- };
-
- /**
- * Convert a property into getter/setter so we can emit
- * the events when the property is accessed/changed.
- *
- * @param {String} key
- * @param {*} val
- */
-
- Observer.prototype.convert = function (key, val) {
- defineReactive(this.value, key, val);
- };
-
- /**
- * Add an owner vm, so that when $set/$delete mutations
- * happen we can notify owner vms to proxy the keys and
- * digest the watchers. This is only called when the object
- * is observed as an instance's root $data.
- *
- * @param {Vue} vm
- */
-
- Observer.prototype.addVm = function (vm) {
- (this.vms || (this.vms = [])).push(vm);
- };
-
- /**
- * Remove an owner vm. This is called when the object is
- * swapped out as an instance's $data object.
- *
- * @param {Vue} vm
- */
-
- Observer.prototype.removeVm = function (vm) {
- this.vms.$remove(vm);
- };
+ }
+ vm.$emit('hook:' + hook);
+}
- // helpers
+/* */
- /**
- * Augment an target Object or Array by intercepting
- * the prototype chain using __proto__
- *
- * @param {Object|Array} target
- * @param {Object} src
- */
+var hooks = { init: init, prepatch: prepatch, insert: insert, destroy: destroy$1 };
+var hooksToMerge = Object.keys(hooks);
- function protoAugment(target, src) {
- /* eslint-disable no-proto */
- target.__proto__ = src;
- /* eslint-enable no-proto */
+function createComponent (
+ Ctor,
+ data,
+ context,
+ children,
+ tag
+) {
+ if (!Ctor) {
+ return
}
- /**
- * Augment an target Object or Array by defining
- * hidden properties.
- *
- * @param {Object|Array} target
- * @param {Object} proto
- */
-
- function copyAugment(target, src, keys) {
- for (var i = 0, l = keys.length; i < l; i++) {
- var key = keys[i];
- def(target, key, src[key]);
- }
+ if (isObject(Ctor)) {
+ Ctor = Vue$3.extend(Ctor);
}
- /**
- * Attempt to create an observer instance for a value,
- * returns the new observer if successfully observed,
- * or the existing observer if the value already has one.
- *
- * @param {*} value
- * @param {Vue} [vm]
- * @return {Observer|undefined}
- * @static
- */
-
- function observe(value, vm) {
- if (!value || typeof value !== 'object') {
- return;
- }
- var ob;
- if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
- ob = value.__ob__;
- } else if (shouldConvert && (isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue) {
- ob = new Observer(value);
- }
- if (ob && vm) {
- ob.addVm(vm);
+ if (typeof Ctor !== 'function') {
+ {
+ warn(("Invalid Component definition: " + (String(Ctor))), context);
}
- return ob;
+ return
}
- /**
- * Define a reactive property on an Object.
- *
- * @param {Object} obj
- * @param {String} key
- * @param {*} val
- */
-
- function defineReactive(obj, key, val) {
- var dep = new Dep();
-
- var property = Object.getOwnPropertyDescriptor(obj, key);
- if (property && property.configurable === false) {
- return;
+ // async component
+ if (!Ctor.cid) {
+ if (Ctor.resolved) {
+ Ctor = Ctor.resolved;
+ } else {
+ Ctor = resolveAsyncComponent(Ctor, function () {
+ // it's ok to queue this on every render because
+ // $forceUpdate is buffered by the scheduler.
+ context.$forceUpdate();
+ });
+ if (!Ctor) {
+ // return nothing if this is indeed an async component
+ // wait for the callback to trigger parent update.
+ return
+ }
+ }
+ }
+
+ data = data || {};
+
+ // extract props
+ var propsData = extractProps(data, Ctor);
+
+ // functional component
+ if (Ctor.options.functional) {
+ return createFunctionalComponent(Ctor, propsData, data, context, children)
+ }
+
+ // extract listeners, since these needs to be treated as
+ // child component listeners instead of DOM listeners
+ var listeners = data.on;
+ // replace with listeners with .native modifier
+ data.on = data.nativeOn;
+
+ if (Ctor.options.abstract) {
+ // abstract components do not keep anything
+ // other than props & listeners
+ data = {};
+ }
+
+ // merge component management hooks onto the placeholder node
+ mergeHooks(data);
+
+ // return a placeholder vnode
+ var name = Ctor.options.name || tag;
+ var vnode = new VNode(
+ ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
+ data, undefined, undefined, undefined, undefined, context,
+ { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }
+ );
+ return vnode
+}
+
+function createFunctionalComponent (
+ Ctor,
+ propsData,
+ data,
+ context,
+ children
+) {
+ var props = {};
+ var propOptions = Ctor.options.props;
+ if (propOptions) {
+ for (var key in propOptions) {
+ props[key] = validateProp(key, propOptions, propsData);
+ }
+ }
+ var vnode = Ctor.options.render.call(
+ null,
+ // ensure the createElement function in functional components
+ // gets a unique context - this is necessary for correct named slot check
+ bind$1(createElement, { _self: Object.create(context) }),
+ {
+ props: props,
+ data: data,
+ parent: context,
+ children: normalizeChildren(children),
+ slots: function () { return resolveSlots(children, context); }
+ }
+ );
+ if (vnode instanceof VNode) {
+ vnode.functionalContext = context;
+ if (data.slot) {
+ (vnode.data || (vnode.data = {})).slot = data.slot;
+ }
+ }
+ return vnode
+}
+
+function createComponentInstanceForVnode (
+ vnode, // we know it's MountedComponentVNode but flow doesn't
+ parent // activeInstance in lifecycle state
+) {
+ var vnodeComponentOptions = vnode.componentOptions;
+ var options = {
+ _isComponent: true,
+ parent: parent,
+ propsData: vnodeComponentOptions.propsData,
+ _componentTag: vnodeComponentOptions.tag,
+ _parentVnode: vnode,
+ _parentListeners: vnodeComponentOptions.listeners,
+ _renderChildren: vnodeComponentOptions.children
+ };
+ // check inline-template render functions
+ var inlineTemplate = vnode.data.inlineTemplate;
+ if (inlineTemplate) {
+ options.render = inlineTemplate.render;
+ options.staticRenderFns = inlineTemplate.staticRenderFns;
+ }
+ return new vnodeComponentOptions.Ctor(options)
+}
+
+function init (vnode, hydrating) {
+ if (!vnode.child || vnode.child._isDestroyed) {
+ var child = vnode.child = createComponentInstanceForVnode(vnode, activeInstance);
+ child.$mount(hydrating ? vnode.elm : undefined, hydrating);
+ }
+}
+
+function prepatch (
+ oldVnode,
+ vnode
+) {
+ var options = vnode.componentOptions;
+ var child = vnode.child = oldVnode.child;
+ child._updateFromParent(
+ options.propsData, // updated props
+ options.listeners, // updated listeners
+ vnode, // new parent vnode
+ options.children // new children
+ );
+}
+
+function insert (vnode) {
+ if (!vnode.child._isMounted) {
+ vnode.child._isMounted = true;
+ callHook(vnode.child, 'mounted');
+ }
+ if (vnode.data.keepAlive) {
+ vnode.child._inactive = false;
+ callHook(vnode.child, 'activated');
+ }
+}
+
+function destroy$1 (vnode) {
+ if (!vnode.child._isDestroyed) {
+ if (!vnode.data.keepAlive) {
+ vnode.child.$destroy();
+ } else {
+ vnode.child._inactive = true;
+ callHook(vnode.child, 'deactivated');
}
-
- // cater for pre-defined getter/setters
- var getter = property && property.get;
- var setter = property && property.set;
-
- var childOb = observe(val);
- Object.defineProperty(obj, key, {
- enumerable: true,
- configurable: true,
- get: function reactiveGetter() {
- var value = getter ? getter.call(obj) : val;
- if (Dep.target) {
- dep.depend();
- if (childOb) {
- childOb.dep.depend();
- }
- if (isArray(value)) {
- for (var e, i = 0, l = value.length; i < l; i++) {
- e = value[i];
- e && e.__ob__ && e.__ob__.dep.depend();
- }
- }
- }
- return value;
- },
- set: function reactiveSetter(newVal) {
- var value = getter ? getter.call(obj) : val;
- if (newVal === value) {
- return;
- }
- if (setter) {
- setter.call(obj, newVal);
- } else {
- val = newVal;
- }
- childOb = observe(newVal);
- dep.notify();
- }
- });
}
+}
-
-
- var util = Object.freeze({
- defineReactive: defineReactive,
- set: set,
- del: del,
- hasOwn: hasOwn,
- isLiteral: isLiteral,
- isReserved: isReserved,
- _toString: _toString,
- toNumber: toNumber,
- toBoolean: toBoolean,
- stripQuotes: stripQuotes,
- camelize: camelize,
- hyphenate: hyphenate,
- classify: classify,
- bind: bind,
- toArray: toArray,
- extend: extend,
- isObject: isObject,
- isPlainObject: isPlainObject,
- def: def,
- debounce: _debounce,
- indexOf: indexOf,
- cancellable: cancellable,
- looseEqual: looseEqual,
- isArray: isArray,
- hasProto: hasProto,
- inBrowser: inBrowser,
- devtools: devtools,
- isIE: isIE,
- isIE9: isIE9,
- isAndroid: isAndroid,
- isIos: isIos,
- iosVersionMatch: iosVersionMatch,
- iosVersion: iosVersion,
- hasMutationObserverBug: hasMutationObserverBug,
- get transitionProp () { return transitionProp; },
- get transitionEndEvent () { return transitionEndEvent; },
- get animationProp () { return animationProp; },
- get animationEndEvent () { return animationEndEvent; },
- nextTick: nextTick,
- get _Set () { return _Set; },
- query: query,
- inDoc: inDoc,
- getAttr: getAttr,
- getBindAttr: getBindAttr,
- hasBindAttr: hasBindAttr,
- before: before,
- after: after,
- remove: remove,
- prepend: prepend,
- replace: replace,
- on: on,
- off: off,
- setClass: setClass,
- addClass: addClass,
- removeClass: removeClass,
- extractContent: extractContent,
- trimNode: trimNode,
- isTemplate: isTemplate,
- createAnchor: createAnchor,
- findRef: findRef,
- mapNodeRange: mapNodeRange,
- removeNodeRange: removeNodeRange,
- isFragment: isFragment,
- getOuterHTML: getOuterHTML,
- mergeOptions: mergeOptions,
- resolveAsset: resolveAsset,
- checkComponentAttr: checkComponentAttr,
- commonTagRE: commonTagRE,
- reservedTagRE: reservedTagRE,
- get warn () { return warn; }
- });
-
- var uid = 0;
-
- function initMixin (Vue) {
- /**
- * The main init sequence. This is called for every
- * instance, including ones that are created from extended
- * constructors.
- *
- * @param {Object} options - this options object should be
- * the result of merging class
- * options and the options passed
- * in to the constructor.
- */
-
- Vue.prototype._init = function (options) {
- options = options || {};
-
- this.$el = null;
- this.$parent = options.parent;
- this.$root = this.$parent ? this.$parent.$root : this;
- this.$children = [];
- this.$refs = {}; // child vm references
- this.$els = {}; // element references
- this._watchers = []; // all watchers as an array
- this._directives = []; // all directives
-
- // a uid
- this._uid = uid++;
-
- // a flag to avoid this being observed
- this._isVue = true;
-
- // events bookkeeping
- this._events = {}; // registered callbacks
- this._eventsCount = {}; // for $broadcast optimization
-
- // fragment instance properties
- this._isFragment = false;
- this._fragment = // @type {DocumentFragment}
- this._fragmentStart = // @type {Text|Comment}
- this._fragmentEnd = null; // @type {Text|Comment}
-
- // lifecycle state
- this._isCompiled = this._isDestroyed = this._isReady = this._isAttached = this._isBeingDestroyed = this._vForRemoving = false;
- this._unlinkFn = null;
-
- // context:
- // if this is a transcluded component, context
- // will be the common parent vm of this instance
- // and its host.
- this._context = options._context || this.$parent;
-
- // scope:
- // if this is inside an inline v-for, the scope
- // will be the intermediate scope created for this
- // repeat fragment. this is used for linking props
- // and container directives.
- this._scope = options._scope;
-
- // fragment:
- // if this instance is compiled inside a Fragment, it
- // needs to reigster itself as a child of that fragment
- // for attach/detach to work properly.
- this._frag = options._frag;
- if (this._frag) {
- this._frag.children.push(this);
- }
-
- // push self into parent / transclusion host
- if (this.$parent) {
- this.$parent.$children.push(this);
- }
-
- // merge options.
- options = this.$options = mergeOptions(this.constructor.options, options, this);
-
- // set ref
- this._updateRef();
-
- // initialize data as empty object.
- // it will be filled up in _initData().
- this._data = {};
-
- // call init hook
- this._callHook('init');
-
- // initialize data observation and scope inheritance.
- this._initState();
-
- // setup event system and option events.
- this._initEvents();
-
- // call created hook
- this._callHook('created');
-
- // if `el` option is passed, start compilation.
- if (options.el) {
- this.$mount(options.el);
+function resolveAsyncComponent (
+ factory,
+ cb
+) {
+ if (factory.requested) {
+ // pool callbacks
+ factory.pendingCallbacks.push(cb);
+ } else {
+ factory.requested = true;
+ var cbs = factory.pendingCallbacks = [cb];
+ var sync = true;
+
+ var resolve = function (res) {
+ if (isObject(res)) {
+ res = Vue$3.extend(res);
+ }
+ // cache resolved
+ factory.resolved = res;
+ // invoke callbacks only if this is not a synchronous resolve
+ // (async resolves are shimmed as synchronous during SSR)
+ if (!sync) {
+ for (var i = 0, l = cbs.length; i < l; i++) {
+ cbs[i](res);
+ }
}
};
- }
-
- var pathCache = new Cache(1000);
-
- // actions
- var APPEND = 0;
- var PUSH = 1;
- var INC_SUB_PATH_DEPTH = 2;
- var PUSH_SUB_PATH = 3;
-
- // states
- var BEFORE_PATH = 0;
- var IN_PATH = 1;
- var BEFORE_IDENT = 2;
- var IN_IDENT = 3;
- var IN_SUB_PATH = 4;
- var IN_SINGLE_QUOTE = 5;
- var IN_DOUBLE_QUOTE = 6;
- var AFTER_PATH = 7;
- var ERROR = 8;
-
- var pathStateMachine = [];
-
- pathStateMachine[BEFORE_PATH] = {
- 'ws': [BEFORE_PATH],
- 'ident': [IN_IDENT, APPEND],
- '[': [IN_SUB_PATH],
- 'eof': [AFTER_PATH]
- };
- pathStateMachine[IN_PATH] = {
- 'ws': [IN_PATH],
- '.': [BEFORE_IDENT],
- '[': [IN_SUB_PATH],
- 'eof': [AFTER_PATH]
- };
-
- pathStateMachine[BEFORE_IDENT] = {
- 'ws': [BEFORE_IDENT],
- 'ident': [IN_IDENT, APPEND]
- };
-
- pathStateMachine[IN_IDENT] = {
- 'ident': [IN_IDENT, APPEND],
- '0': [IN_IDENT, APPEND],
- 'number': [IN_IDENT, APPEND],
- 'ws': [IN_PATH, PUSH],
- '.': [BEFORE_IDENT, PUSH],
- '[': [IN_SUB_PATH, PUSH],
- 'eof': [AFTER_PATH, PUSH]
- };
-
- pathStateMachine[IN_SUB_PATH] = {
- "'": [IN_SINGLE_QUOTE, APPEND],
- '"': [IN_DOUBLE_QUOTE, APPEND],
- '[': [IN_SUB_PATH, INC_SUB_PATH_DEPTH],
- ']': [IN_PATH, PUSH_SUB_PATH],
- 'eof': ERROR,
- 'else': [IN_SUB_PATH, APPEND]
- };
-
- pathStateMachine[IN_SINGLE_QUOTE] = {
- "'": [IN_SUB_PATH, APPEND],
- 'eof': ERROR,
- 'else': [IN_SINGLE_QUOTE, APPEND]
- };
-
- pathStateMachine[IN_DOUBLE_QUOTE] = {
- '"': [IN_SUB_PATH, APPEND],
- 'eof': ERROR,
- 'else': [IN_DOUBLE_QUOTE, APPEND]
- };
-
- /**
- * Determine the type of a character in a keypath.
- *
- * @param {Char} ch
- * @return {String} type
- */
-
- function getPathCharType(ch) {
- if (ch === undefined) {
- return 'eof';
- }
-
- var code = ch.charCodeAt(0);
-
- switch (code) {
- case 0x5B: // [
- case 0x5D: // ]
- case 0x2E: // .
- case 0x22: // "
- case 0x27: // '
- case 0x30:
- // 0
- return ch;
-
- case 0x5F: // _
- case 0x24:
- // $
- return 'ident';
+ var reject = function (reason) {
+ "development" !== 'production' && warn(
+ "Failed to resolve async component: " + (String(factory)) +
+ (reason ? ("\nReason: " + reason) : '')
+ );
+ };
- case 0x20: // Space
- case 0x09: // Tab
- case 0x0A: // Newline
- case 0x0D: // Return
- case 0xA0: // No-break space
- case 0xFEFF: // Byte Order Mark
- case 0x2028: // Line Separator
- case 0x2029:
- // Paragraph Separator
- return 'ws';
+ var res = factory(resolve, reject);
+
+ // handle promise
+ if (res && typeof res.then === 'function' && !factory.resolved) {
+ res.then(resolve, reject);
+ }
+
+ sync = false;
+ // return in case resolved synchronously
+ return factory.resolved
+ }
+}
+
+function extractProps (data, Ctor) {
+ // we are only extrating raw values here.
+ // validation and default values are handled in the child
+ // component itself.
+ var propOptions = Ctor.options.props;
+ if (!propOptions) {
+ return
+ }
+ var res = {};
+ var attrs = data.attrs;
+ var props = data.props;
+ var domProps = data.domProps;
+ if (attrs || props || domProps) {
+ for (var key in propOptions) {
+ var altKey = hyphenate(key);
+ checkProp(res, props, key, altKey, true) ||
+ checkProp(res, attrs, key, altKey) ||
+ checkProp(res, domProps, key, altKey);
+ }
+ }
+ return res
+}
+
+function checkProp (
+ res,
+ hash,
+ key,
+ altKey,
+ preserve
+) {
+ if (hash) {
+ if (hasOwn(hash, key)) {
+ res[key] = hash[key];
+ if (!preserve) {
+ delete hash[key];
+ }
+ return true
+ } else if (hasOwn(hash, altKey)) {
+ res[key] = hash[altKey];
+ if (!preserve) {
+ delete hash[altKey];
+ }
+ return true
+ }
+ }
+ return false
+}
+
+function mergeHooks (data) {
+ if (!data.hook) {
+ data.hook = {};
+ }
+ for (var i = 0; i < hooksToMerge.length; i++) {
+ var key = hooksToMerge[i];
+ var fromParent = data.hook[key];
+ var ours = hooks[key];
+ data.hook[key] = fromParent ? mergeHook$1(ours, fromParent) : ours;
+ }
+}
+
+function mergeHook$1 (a, b) {
+ // since all hooks have at most two args, use fixed args
+ // to avoid having to use fn.apply().
+ return function (_, __) {
+ a(_, __);
+ b(_, __);
+ }
+}
+
+/* */
+
+// wrapper function for providing a more flexible interface
+// without getting yelled at by flow
+function createElement (
+ tag,
+ data,
+ children
+) {
+ if (data && (Array.isArray(data) || typeof data !== 'object')) {
+ children = data;
+ data = undefined;
+ }
+ // make sure to use real instance instead of proxy as context
+ return _createElement(this._self, tag, data, children)
+}
+
+function _createElement (
+ context,
+ tag,
+ data,
+ children
+) {
+ if (data && data.__ob__) {
+ "development" !== 'production' && warn(
+ "Avoid using observed data object as vnode data: " + (JSON.stringify(data)) + "\n" +
+ 'Always create fresh vnode data objects in each render!',
+ context
+ );
+ return
+ }
+ if (!tag) {
+ // in case of component :is set to falsy value
+ return emptyVNode()
+ }
+ if (typeof tag === 'string') {
+ var Ctor;
+ var ns = config.getTagNamespace(tag);
+ if (config.isReservedTag(tag)) {
+ // platform built-in elements
+ return new VNode(
+ tag, data, normalizeChildren(children, ns),
+ undefined, undefined, ns, context
+ )
+ } else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
+ // component
+ return createComponent(Ctor, data, context, children, tag)
+ } else {
+ // unknown or unlisted namespaced elements
+ // check at runtime because it may get assigned a namespace when its
+ // parent normalizes children
+ return new VNode(
+ tag, data, normalizeChildren(children, ns),
+ undefined, undefined, ns, context
+ )
}
+ } else {
+ // direct component options / constructor
+ return createComponent(tag, data, context, children)
+ }
+}
+
+/* */
+
+function initRender (vm) {
+ vm.$vnode = null; // the placeholder node in parent tree
+ vm._vnode = null; // the root of the child tree
+ vm._staticTrees = null;
+ vm._renderContext = vm.$options._parentVnode && vm.$options._parentVnode.context;
+ vm.$slots = resolveSlots(vm.$options._renderChildren, vm._renderContext);
+ // bind the public createElement fn to this instance
+ // so that we get proper render context inside it.
+ vm.$createElement = bind$1(createElement, vm);
+ if (vm.$options.el) {
+ vm.$mount(vm.$options.el);
+ }
+}
+
+function renderMixin (Vue) {
+ Vue.prototype.$nextTick = function (fn) {
+ nextTick(fn, this);
+ };
- // a-z, A-Z
- if (code >= 0x61 && code <= 0x7A || code >= 0x41 && code <= 0x5A) {
- return 'ident';
- }
+ Vue.prototype._render = function () {
+ var vm = this;
+ var ref = vm.$options;
+ var render = ref.render;
+ var staticRenderFns = ref.staticRenderFns;
+ var _parentVnode = ref._parentVnode;
- // 1-9
- if (code >= 0x31 && code <= 0x39) {
- return 'number';
+ if (vm._isMounted) {
+ // clone slot nodes on re-renders
+ for (var key in vm.$slots) {
+ vm.$slots[key] = cloneVNodes(vm.$slots[key]);
+ }
}
- return 'else';
- }
-
- /**
- * Format a subPath, return its plain form if it is
- * a literal string or number. Otherwise prepend the
- * dynamic indicator (*).
- *
- * @param {String} path
- * @return {String}
- */
-
- function formatSubPath(path) {
- var trimmed = path.trim();
- // invalid leading 0
- if (path.charAt(0) === '0' && isNaN(path)) {
- return false;
+ if (staticRenderFns && !vm._staticTrees) {
+ vm._staticTrees = [];
}
- return isLiteral(trimmed) ? stripQuotes(trimmed) : '*' + trimmed;
- }
-
- /**
- * Parse a string path into an array of segments
- *
- * @param {String} path
- * @return {Array|undefined}
- */
-
- function parse(path) {
- var keys = [];
- var index = -1;
- var mode = BEFORE_PATH;
- var subPathDepth = 0;
- var c, newChar, key, type, transition, action, typeMap;
-
- var actions = [];
-
- actions[PUSH] = function () {
- if (key !== undefined) {
- keys.push(key);
- key = undefined;
- }
- };
-
- actions[APPEND] = function () {
- if (key === undefined) {
- key = newChar;
- } else {
- key += newChar;
+ // set parent vnode. this allows render functions to have access
+ // to the data on the placeholder node.
+ vm.$vnode = _parentVnode;
+ // render self
+ var vnode;
+ try {
+ vnode = render.call(vm._renderProxy, vm.$createElement);
+ } catch (e) {
+ {
+ warn(("Error when rendering " + (formatComponentName(vm)) + ":"));
}
- };
-
- actions[INC_SUB_PATH_DEPTH] = function () {
- actions[APPEND]();
- subPathDepth++;
- };
-
- actions[PUSH_SUB_PATH] = function () {
- if (subPathDepth > 0) {
- subPathDepth--;
- mode = IN_SUB_PATH;
- actions[APPEND]();
+ /* istanbul ignore else */
+ if (config.errorHandler) {
+ config.errorHandler.call(null, e, vm);
} else {
- subPathDepth = 0;
- key = formatSubPath(key);
- if (key === false) {
- return false;
+ if (config._isServer) {
+ throw e
} else {
- actions[PUSH]();
+ setTimeout(function () { throw e }, 0);
}
}
- };
-
- function maybeUnescapeQuote() {
- var nextChar = path[index + 1];
- if (mode === IN_SINGLE_QUOTE && nextChar === "'" || mode === IN_DOUBLE_QUOTE && nextChar === '"') {
- index++;
- newChar = '\\' + nextChar;
- actions[APPEND]();
- return true;
- }
+ // return previous vnode to prevent render error causing blank component
+ vnode = vm._vnode;
}
-
- while (mode != null) {
- index++;
- c = path[index];
-
- if (c === '\\' && maybeUnescapeQuote()) {
- continue;
- }
-
- type = getPathCharType(c);
- typeMap = pathStateMachine[mode];
- transition = typeMap[type] || typeMap['else'] || ERROR;
-
- if (transition === ERROR) {
- return; // parse error
+ // return empty vnode in case the render function errored out
+ if (!(vnode instanceof VNode)) {
+ if ("development" !== 'production' && Array.isArray(vnode)) {
+ warn(
+ 'Multiple root nodes returned from render function. Render function ' +
+ 'should return a single root node.',
+ vm
+ );
}
+ vnode = emptyVNode();
+ }
+ // set parent
+ vnode.parent = _parentVnode;
+ return vnode
+ };
- mode = transition[0];
- action = actions[transition[1]];
- if (action) {
- newChar = transition[2];
- newChar = newChar === undefined ? c : newChar;
- if (action() === false) {
- return;
+ // shorthands used in render functions
+ Vue.prototype._h = createElement;
+ // toString for mustaches
+ Vue.prototype._s = _toString;
+ // number conversion
+ Vue.prototype._n = toNumber;
+ // empty vnode
+ Vue.prototype._e = emptyVNode;
+ // loose equal
+ Vue.prototype._q = looseEqual;
+ // loose indexOf
+ Vue.prototype._i = looseIndexOf;
+
+ // render static tree by index
+ Vue.prototype._m = function renderStatic (
+ index,
+ isInFor
+ ) {
+ var tree = this._staticTrees[index];
+ // if has already-rendered static tree and not inside v-for,
+ // we can reuse the same tree by doing a shallow clone.
+ if (tree && !isInFor) {
+ return Array.isArray(tree)
+ ? cloneVNodes(tree)
+ : cloneVNode(tree)
+ }
+ // otherwise, render a fresh tree.
+ tree = this._staticTrees[index] = this.$options.staticRenderFns[index].call(this._renderProxy);
+ if (Array.isArray(tree)) {
+ for (var i = 0; i < tree.length; i++) {
+ if (typeof tree[i] !== 'string') {
+ tree[i].isStatic = true;
+ tree[i].key = "__static__" + index + "_" + i;
}
}
-
- if (mode === AFTER_PATH) {
- keys.raw = path;
- return keys;
- }
+ } else {
+ tree.isStatic = true;
+ tree.key = "__static__" + index;
}
- }
+ return tree
+ };
- /**
- * External parse that check for a cache hit first
- *
- * @param {String} path
- * @return {Array|undefined}
- */
+ // filter resolution helper
+ var identity = function (_) { return _; };
+ Vue.prototype._f = function resolveFilter (id) {
+ return resolveAsset(this.$options, 'filters', id, true) || identity
+ };
- function parsePath(path) {
- var hit = pathCache.get(path);
- if (!hit) {
- hit = parse(path);
- if (hit) {
- pathCache.put(path, hit);
+ // render v-for
+ Vue.prototype._l = function renderList (
+ val,
+ render
+ ) {
+ var ret, i, l, keys, key;
+ if (Array.isArray(val)) {
+ ret = new Array(val.length);
+ for (i = 0, l = val.length; i < l; i++) {
+ ret[i] = render(val[i], i);
+ }
+ } else if (typeof val === 'number') {
+ ret = new Array(val);
+ for (i = 0; i < val; i++) {
+ ret[i] = render(i + 1, i);
+ }
+ } else if (isObject(val)) {
+ keys = Object.keys(val);
+ ret = new Array(keys.length);
+ for (i = 0, l = keys.length; i < l; i++) {
+ key = keys[i];
+ ret[i] = render(val[key], key, i);
}
}
- return hit;
- }
-
- /**
- * Get from an object from a path string
- *
- * @param {Object} obj
- * @param {String} path
- */
-
- function getPath(obj, path) {
- return parseExpression(path).get(obj);
- }
-
- /**
- * Warn against setting non-existent root path on a vm.
- */
-
- var warnNonExistent;
- if ('development' !== 'production') {
- warnNonExistent = function (path, vm) {
- warn('You are setting a non-existent path "' + path.raw + '" ' + 'on a vm instance. Consider pre-initializing the property ' + 'with the "data" option for more reliable reactivity ' + 'and better performance.', vm);
- };
- }
+ return ret
+ };
- /**
- * Set on an object from a path
- *
- * @param {Object} obj
- * @param {String | Array} path
- * @param {*} val
- */
+ // renderSlot
+ Vue.prototype._t = function (
+ name,
+ fallback
+ ) {
+ var slotNodes = this.$slots[name];
+ // warn duplicate slot usage
+ if (slotNodes && "development" !== 'production') {
+ slotNodes._rendered && warn(
+ "Duplicate presence of slot \"" + name + "\" found in the same render tree " +
+ "- this will likely cause render errors.",
+ this
+ );
+ slotNodes._rendered = true;
+ }
+ return slotNodes || fallback
+ };
- function setPath(obj, path, val) {
- var original = obj;
- if (typeof path === 'string') {
- path = parse(path);
- }
- if (!path || !isObject(obj)) {
- return false;
- }
- var last, key;
- for (var i = 0, l = path.length; i < l; i++) {
- last = obj;
- key = path[i];
- if (key.charAt(0) === '*') {
- key = parseExpression(key.slice(1)).get.call(original, original);
- }
- if (i < l - 1) {
- obj = obj[key];
- if (!isObject(obj)) {
- obj = {};
- if ('development' !== 'production' && last._isVue) {
- warnNonExistent(path, last);
- }
- set(last, key, obj);
- }
+ // apply v-bind object
+ Vue.prototype._b = function bindProps (
+ data,
+ value,
+ asProp
+ ) {
+ if (value) {
+ if (!isObject(value)) {
+ "development" !== 'production' && warn(
+ 'v-bind without argument expects an Object or Array value',
+ this
+ );
} else {
- if (isArray(obj)) {
- obj.$set(key, val);
- } else if (key in obj) {
- obj[key] = val;
- } else {
- if ('development' !== 'production' && obj._isVue) {
- warnNonExistent(path, obj);
+ if (Array.isArray(value)) {
+ value = toObject(value);
+ }
+ for (var key in value) {
+ if (key === 'class' || key === 'style') {
+ data[key] = value[key];
+ } else {
+ var hash = asProp || config.mustUseProp(key)
+ ? data.domProps || (data.domProps = {})
+ : data.attrs || (data.attrs = {});
+ hash[key] = value[key];
}
- set(obj, key, val);
}
}
}
- return true;
- }
-
-var path = Object.freeze({
- parsePath: parsePath,
- getPath: getPath,
- setPath: setPath
- });
-
- var expressionCache = new Cache(1000);
-
- var allowedKeywords = 'Math,Date,this,true,false,null,undefined,Infinity,NaN,' + 'isNaN,isFinite,decodeURI,decodeURIComponent,encodeURI,' + 'encodeURIComponent,parseInt,parseFloat';
- var allowedKeywordsRE = new RegExp('^(' + allowedKeywords.replace(/,/g, '\\b|') + '\\b)');
-
- // keywords that don't make sense inside expressions
- var improperKeywords = 'break,case,class,catch,const,continue,debugger,default,' + 'delete,do,else,export,extends,finally,for,function,if,' + 'import,in,instanceof,let,return,super,switch,throw,try,' + 'var,while,with,yield,enum,await,implements,package,' + 'protected,static,interface,private,public';
- var improperKeywordsRE = new RegExp('^(' + improperKeywords.replace(/,/g, '\\b|') + '\\b)');
-
- var wsRE = /\s/g;
- var newlineRE = /\n/g;
- var saveRE = /[\{,]\s*[\w\$_]+\s*:|('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`)|new |typeof |void /g;
- var restoreRE = /"(\d+)"/g;
- var pathTestRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*$/;
- var identRE = /[^\w$\.](?:[A-Za-z_$][\w$]*)/g;
- var literalValueRE$1 = /^(?:true|false|null|undefined|Infinity|NaN)$/;
-
- function noop() {}
-
- /**
- * Save / Rewrite / Restore
- *
- * When rewriting paths found in an expression, it is
- * possible for the same letter sequences to be found in
- * strings and Object literal property keys. Therefore we
- * remove and store these parts in a temporary array, and
- * restore them after the path rewrite.
- */
-
- var saved = [];
-
- /**
- * Save replacer
- *
- * The save regex can match two possible cases:
- * 1. An opening object literal
- * 2. A string
- * If matched as a plain string, we need to escape its
- * newlines, since the string needs to be preserved when
- * generating the function body.
- *
- * @param {String} str
- * @param {String} isString - str if matched as a string
- * @return {String} - placeholder with index
- */
-
- function save(str, isString) {
- var i = saved.length;
- saved[i] = isString ? str.replace(newlineRE, '\\n') : str;
- return '"' + i + '"';
- }
-
- /**
- * Path rewrite replacer
- *
- * @param {String} raw
- * @return {String}
- */
+ return data
+ };
- function rewrite(raw) {
- var c = raw.charAt(0);
- var path = raw.slice(1);
- if (allowedKeywordsRE.test(path)) {
- return raw;
+ // expose v-on keyCodes
+ Vue.prototype._k = function getKeyCodes (key) {
+ return config.keyCodes[key]
+ };
+}
+
+function resolveSlots (
+ renderChildren,
+ context
+) {
+ var slots = {};
+ if (!renderChildren) {
+ return slots
+ }
+ var children = normalizeChildren(renderChildren) || [];
+ var defaultSlot = [];
+ var name, child;
+ for (var i = 0, l = children.length; i < l; i++) {
+ child = children[i];
+ // named slots should only be respected if the vnode was rendered in the
+ // same context.
+ if ((child.context === context || child.functionalContext === context) &&
+ child.data && (name = child.data.slot)) {
+ var slot = (slots[name] || (slots[name] = []));
+ if (child.tag === 'template') {
+ slot.push.apply(slot, child.children);
+ } else {
+ slot.push(child);
+ }
} else {
- path = path.indexOf('"') > -1 ? path.replace(restoreRE, restore) : path;
- return c + 'scope.' + path;
+ defaultSlot.push(child);
}
}
-
- /**
- * Restore replacer
- *
- * @param {String} str
- * @param {String} i - matched save index
- * @return {String}
- */
-
- function restore(str, i) {
- return saved[i];
+ // ignore single whitespace
+ if (defaultSlot.length && !(
+ defaultSlot.length === 1 &&
+ (defaultSlot[0].text === ' ' || defaultSlot[0].isComment)
+ )) {
+ slots.default = defaultSlot;
}
+ return slots
+}
- /**
- * Rewrite an expression, prefixing all path accessors with
- * `scope.` and generate getter/setter functions.
- *
- * @param {String} exp
- * @return {Function}
- */
+/* */
- function compileGetter(exp) {
- if (improperKeywordsRE.test(exp)) {
- 'development' !== 'production' && warn('Avoid using reserved keywords in expression: ' + exp);
- }
- // reset state
- saved.length = 0;
- // save strings and object literal keys
- var body = exp.replace(saveRE, save).replace(wsRE, '');
- // rewrite all paths
- // pad 1 space here because the regex matches 1 extra char
- body = (' ' + body).replace(identRE, rewrite).replace(restoreRE, restore);
- return makeGetterFn(body);
+function initEvents (vm) {
+ vm._events = Object.create(null);
+ // init parent attached events
+ var listeners = vm.$options._parentListeners;
+ var on = bind$1(vm.$on, vm);
+ var off = bind$1(vm.$off, vm);
+ vm._updateListeners = function (listeners, oldListeners) {
+ updateListeners(listeners, oldListeners || {}, on, off, vm);
+ };
+ if (listeners) {
+ vm._updateListeners(listeners);
}
+}
- /**
- * Build a getter function. Requires eval.
- *
- * We isolate the try/catch so it doesn't affect the
- * optimization of the parse function when it is not called.
- *
- * @param {String} body
- * @return {Function|undefined}
- */
+function eventsMixin (Vue) {
+ Vue.prototype.$on = function (event, fn) {
+ var vm = this;(vm._events[event] || (vm._events[event] = [])).push(fn);
+ return vm
+ };
- function makeGetterFn(body) {
- try {
- /* eslint-disable no-new-func */
- return new Function('scope', 'return ' + body + ';');
- /* eslint-enable no-new-func */
- } catch (e) {
- if ('development' !== 'production') {
- /* istanbul ignore if */
- if (e.toString().match(/unsafe-eval|CSP/)) {
- warn('It seems you are using the default build of Vue.js in an environment ' + 'with Content Security Policy that prohibits unsafe-eval. ' + 'Use the CSP-compliant build instead: ' + 'http://vuejs.org/guide/installation.html#CSP-compliant-build');
- } else {
- warn('Invalid expression. ' + 'Generated function body: ' + body);
- }
- }
- return noop;
+ Vue.prototype.$once = function (event, fn) {
+ var vm = this;
+ function on () {
+ vm.$off(event, on);
+ fn.apply(vm, arguments);
}
- }
-
- /**
- * Compile a setter function for the expression.
- *
- * @param {String} exp
- * @return {Function|undefined}
- */
+ on.fn = fn;
+ vm.$on(event, on);
+ return vm
+ };
- function compileSetter(exp) {
- var path = parsePath(exp);
- if (path) {
- return function (scope, val) {
- setPath(scope, path, val);
- };
- } else {
- 'development' !== 'production' && warn('Invalid setter expression: ' + exp);
+ Vue.prototype.$off = function (event, fn) {
+ var vm = this;
+ // all
+ if (!arguments.length) {
+ vm._events = Object.create(null);
+ return vm
+ }
+ // specific event
+ var cbs = vm._events[event];
+ if (!cbs) {
+ return vm
+ }
+ if (arguments.length === 1) {
+ vm._events[event] = null;
+ return vm
+ }
+ // specific handler
+ var cb;
+ var i = cbs.length;
+ while (i--) {
+ cb = cbs[i];
+ if (cb === fn || cb.fn === fn) {
+ cbs.splice(i, 1);
+ break
+ }
}
- }
-
- /**
- * Parse an expression into re-written getter/setters.
- *
- * @param {String} exp
- * @param {Boolean} needSet
- * @return {Function}
- */
+ return vm
+ };
- function parseExpression(exp, needSet) {
- exp = exp.trim();
- // try cache
- var hit = expressionCache.get(exp);
- if (hit) {
- if (needSet && !hit.set) {
- hit.set = compileSetter(hit.exp);
+ Vue.prototype.$emit = function (event) {
+ var vm = this;
+ var cbs = vm._events[event];
+ if (cbs) {
+ cbs = cbs.length > 1 ? toArray(cbs) : cbs;
+ var args = toArray(arguments, 1);
+ for (var i = 0, l = cbs.length; i < l; i++) {
+ cbs[i].apply(vm, args);
}
- return hit;
}
- var res = { exp: exp };
- res.get = isSimplePath(exp) && exp.indexOf('[') < 0
- // optimized super simple getter
- ? makeGetterFn('scope.' + exp)
- // dynamic getter
- : compileGetter(exp);
- if (needSet) {
- res.set = compileSetter(exp);
+ return vm
+ };
+}
+
+/* */
+
+var uid = 0;
+
+function initMixin (Vue) {
+ Vue.prototype._init = function (options) {
+ var vm = this;
+ // a uid
+ vm._uid = uid++;
+ // a flag to avoid this being observed
+ vm._isVue = true;
+ // merge options
+ if (options && options._isComponent) {
+ // optimize internal component instantiation
+ // since dynamic options merging is pretty slow, and none of the
+ // internal component options needs special treatment.
+ initInternalComponent(vm, options);
+ } else {
+ vm.$options = mergeOptions(
+ resolveConstructorOptions(vm),
+ options || {},
+ vm
+ );
}
- expressionCache.put(exp, res);
- return res;
- }
-
- /**
- * Check if an expression is a simple path.
- *
- * @param {String} exp
- * @return {Boolean}
- */
-
- function isSimplePath(exp) {
- return pathTestRE.test(exp) &&
- // don't treat literal values as paths
- !literalValueRE$1.test(exp) &&
- // Math constants e.g. Math.PI, Math.E etc.
- exp.slice(0, 5) !== 'Math.';
- }
-
-var expression = Object.freeze({
- parseExpression: parseExpression,
- isSimplePath: isSimplePath
- });
-
- // we have two separate queues: one for directive updates
- // and one for user watcher registered via $watch().
- // we want to guarantee directive updates to be called
- // before user watchers so that when user watchers are
- // triggered, the DOM would have already been in updated
- // state.
-
- var queue = [];
- var userQueue = [];
- var has = {};
- var circular = {};
- var waiting = false;
-
- /**
- * Reset the batcher's state.
- */
-
- function resetBatcherState() {
- queue.length = 0;
- userQueue.length = 0;
- has = {};
- circular = {};
- waiting = false;
- }
-
- /**
- * Flush both queues and run the watchers.
- */
-
- function flushBatcherQueue() {
- var _again = true;
-
- _function: while (_again) {
- _again = false;
+ /* istanbul ignore else */
+ {
+ initProxy(vm);
+ }
+ // expose real self
+ vm._self = vm;
+ initLifecycle(vm);
+ initEvents(vm);
+ callHook(vm, 'beforeCreate');
+ initState(vm);
+ callHook(vm, 'created');
+ initRender(vm);
+ };
- runBatcherQueue(queue);
- runBatcherQueue(userQueue);
- // user watchers triggered more watchers,
- // keep flushing until it depletes
- if (queue.length) {
- _again = true;
- continue _function;
- }
- // dev tool hook
- /* istanbul ignore if */
- if (devtools && config.devtools) {
- devtools.emit('flush');
- }
- resetBatcherState();
+ function initInternalComponent (vm, options) {
+ var opts = vm.$options = Object.create(resolveConstructorOptions(vm));
+ // doing this because it's faster than dynamic enumeration.
+ opts.parent = options.parent;
+ opts.propsData = options.propsData;
+ opts._parentVnode = options._parentVnode;
+ opts._parentListeners = options._parentListeners;
+ opts._renderChildren = options._renderChildren;
+ opts._componentTag = options._componentTag;
+ if (options.render) {
+ opts.render = options.render;
+ opts.staticRenderFns = options.staticRenderFns;
}
}
- /**
- * Run the watchers in a single queue.
- *
- * @param {Array} queue
- */
-
- function runBatcherQueue(queue) {
- // do not cache length because more watchers might be pushed
- // as we run existing watchers
- for (var i = 0; i < queue.length; i++) {
- var watcher = queue[i];
- var id = watcher.id;
- has[id] = null;
- watcher.run();
- // in dev build, check and stop circular updates.
- if ('development' !== 'production' && has[id] != null) {
- circular[id] = (circular[id] || 0) + 1;
- if (circular[id] > config._maxUpdateCount) {
- warn('You may have an infinite update loop for watcher ' + 'with expression "' + watcher.expression + '"', watcher.vm);
- break;
+ function resolveConstructorOptions (vm) {
+ var Ctor = vm.constructor;
+ var options = Ctor.options;
+ if (Ctor.super) {
+ var superOptions = Ctor.super.options;
+ var cachedSuperOptions = Ctor.superOptions;
+ if (superOptions !== cachedSuperOptions) {
+ // super option changed
+ Ctor.superOptions = superOptions;
+ options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions);
+ if (options.name) {
+ options.components[options.name] = Ctor;
}
}
}
- queue.length = 0;
+ return options
}
+}
- /**
- * Push a watcher into the watcher queue.
- * Jobs with duplicate IDs will be skipped unless it's
- * pushed when the queue is being flushed.
- *
- * @param {Watcher} watcher
- * properties:
- * - {Number} id
- * - {Function} run
- */
-
- function pushWatcher(watcher) {
- var id = watcher.id;
- if (has[id] == null) {
- // push watcher into appropriate queue
- var q = watcher.user ? userQueue : queue;
- has[id] = q.length;
- q.push(watcher);
- // queue the flush
- if (!waiting) {
- waiting = true;
- nextTick(flushBatcherQueue);
- }
- }
- }
-
- var uid$2 = 0;
-
- /**
- * A watcher parses an expression, collects dependencies,
- * and fires callback when the expression value changes.
- * This is used for both the $watch() api and directives.
- *
- * @param {Vue} vm
- * @param {String|Function} expOrFn
- * @param {Function} cb
- * @param {Object} options
- * - {Array} filters
- * - {Boolean} twoWay
- * - {Boolean} deep
- * - {Boolean} user
- * - {Boolean} sync
- * - {Boolean} lazy
- * - {Function} [preProcess]
- * - {Function} [postProcess]
- * @constructor
- */
- function Watcher(vm, expOrFn, cb, options) {
- // mix in options
- if (options) {
- extend(this, options);
- }
- var isFn = typeof expOrFn === 'function';
- this.vm = vm;
- vm._watchers.push(this);
- this.expression = expOrFn;
- this.cb = cb;
- this.id = ++uid$2; // uid for batching
- this.active = true;
- this.dirty = this.lazy; // for lazy watchers
- this.deps = [];
- this.newDeps = [];
- this.depIds = new _Set();
- this.newDepIds = new _Set();
- this.prevError = null; // for async error stacks
- // parse expression for getter/setter
- if (isFn) {
- this.getter = expOrFn;
- this.setter = undefined;
- } else {
- var res = parseExpression(expOrFn, this.twoWay);
- this.getter = res.get;
- this.setter = res.set;
- }
- this.value = this.lazy ? undefined : this.get();
- // state for avoiding false triggers for deep and Array
- // watchers during vm._digest()
- this.queued = this.shallow = false;
+function Vue$3 (options) {
+ if ("development" !== 'production' &&
+ !(this instanceof Vue$3)) {
+ warn('Vue is a constructor and should be called with the `new` keyword');
}
+ this._init(options);
+}
- /**
- * Evaluate the getter, and re-collect dependencies.
- */
+initMixin(Vue$3);
+stateMixin(Vue$3);
+eventsMixin(Vue$3);
+lifecycleMixin(Vue$3);
+renderMixin(Vue$3);
- Watcher.prototype.get = function () {
- this.beforeGet();
- var scope = this.scope || this.vm;
- var value;
- try {
- value = this.getter.call(scope, scope);
- } catch (e) {
- if ('development' !== 'production' && config.warnExpressionErrors) {
- warn('Error when evaluating expression ' + '"' + this.expression + '": ' + e.toString(), this.vm);
- }
- }
- // "touch" every property so they are all tracked as
- // dependencies for deep watching
- if (this.deep) {
- traverse(value);
- }
- if (this.preProcess) {
- value = this.preProcess(value);
- }
- if (this.filters) {
- value = scope._applyFilters(value, null, this.filters, false);
- }
- if (this.postProcess) {
- value = this.postProcess(value);
- }
- this.afterGet();
- return value;
- };
+var warn = noop;
+var formatComponentName;
- /**
- * Set the corresponding value with the setter.
- *
- * @param {*} value
- */
+{
+ var hasConsole = typeof console !== 'undefined';
- Watcher.prototype.set = function (value) {
- var scope = this.scope || this.vm;
- if (this.filters) {
- value = scope._applyFilters(value, this.value, this.filters, true);
- }
- try {
- this.setter.call(scope, scope, value);
- } catch (e) {
- if ('development' !== 'production' && config.warnExpressionErrors) {
- warn('Error when evaluating setter ' + '"' + this.expression + '": ' + e.toString(), this.vm);
- }
- }
- // two-way sync for v-for alias
- var forContext = scope.$forContext;
- if (forContext && forContext.alias === this.expression) {
- if (forContext.filters) {
- 'development' !== 'production' && warn('It seems you are using two-way binding on ' + 'a v-for alias (' + this.expression + '), and the ' + 'v-for has filters. This will not work properly. ' + 'Either remove the filters or use an array of ' + 'objects and bind to object properties instead.', this.vm);
- return;
- }
- forContext._withLock(function () {
- if (scope.$key) {
- // original is an object
- forContext.rawValue[scope.$key] = value;
- } else {
- forContext.rawValue.$set(scope.$index, value);
- }
- });
+ warn = function (msg, vm) {
+ if (hasConsole && (!config.silent)) {
+ console.error("[Vue warn]: " + msg + " " + (
+ vm ? formatLocation(formatComponentName(vm)) : ''
+ ));
}
};
- /**
- * Prepare for dependency collection.
- */
-
- Watcher.prototype.beforeGet = function () {
- Dep.target = this;
+ formatComponentName = function (vm) {
+ if (vm.$root === vm) {
+ return 'root instance'
+ }
+ var name = vm._isVue
+ ? vm.$options.name || vm.$options._componentTag
+ : vm.name;
+ return (
+ (name ? ("component <" + name + ">") : "anonymous component") +
+ (vm._isVue && vm.$options.__file ? (" at " + (vm.$options.__file)) : '')
+ )
};
- /**
- * Add a dependency to this directive.
- *
- * @param {Dep} dep
- */
-
- Watcher.prototype.addDep = function (dep) {
- var id = dep.id;
- if (!this.newDepIds.has(id)) {
- this.newDepIds.add(id);
- this.newDeps.push(dep);
- if (!this.depIds.has(id)) {
- dep.addSub(this);
- }
+ var formatLocation = function (str) {
+ if (str === 'anonymous component') {
+ str += " - use the \"name\" option for better debugging messages.";
}
+ return ("\n(found in " + str + ")")
};
+}
- /**
- * Clean up for dependency collection.
- */
-
- Watcher.prototype.afterGet = function () {
- Dep.target = null;
- var i = this.deps.length;
- while (i--) {
- var dep = this.deps[i];
- if (!this.newDepIds.has(dep.id)) {
- dep.removeSub(this);
- }
- }
- var tmp = this.depIds;
- this.depIds = this.newDepIds;
- this.newDepIds = tmp;
- this.newDepIds.clear();
- tmp = this.deps;
- this.deps = this.newDeps;
- this.newDeps = tmp;
- this.newDeps.length = 0;
- };
+/* */
- /**
- * Subscriber interface.
- * Will be called when a dependency changes.
- *
- * @param {Boolean} shallow
- */
+/**
+ * Option overwriting strategies are functions that handle
+ * how to merge a parent option value and a child option
+ * value into the final value.
+ */
+var strats = config.optionMergeStrategies;
- Watcher.prototype.update = function (shallow) {
- if (this.lazy) {
- this.dirty = true;
- } else if (this.sync || !config.async) {
- this.run();
- } else {
- // if queued, only overwrite shallow with non-shallow,
- // but not the other way around.
- this.shallow = this.queued ? shallow ? this.shallow : false : !!shallow;
- this.queued = true;
- // record before-push error stack in debug mode
- /* istanbul ignore if */
- if ('development' !== 'production' && config.debug) {
- this.prevError = new Error('[vue] async stack trace');
- }
- pushWatcher(this);
+/**
+ * Options with restrictions
+ */
+{
+ strats.el = strats.propsData = function (parent, child, vm, key) {
+ if (!vm) {
+ warn(
+ "option \"" + key + "\" can only be used during instance " +
+ 'creation with the `new` keyword.'
+ );
}
+ return defaultStrat(parent, child)
};
+}
- /**
- * Batcher job interface.
- * Will be called by the batcher.
- */
-
- Watcher.prototype.run = function () {
- if (this.active) {
- var value = this.get();
- if (value !== this.value ||
- // Deep watchers and watchers on Object/Arrays should fire even
- // when the value is the same, because the value may
- // have mutated; but only do so if this is a
- // non-shallow update (caused by a vm digest).
- (isObject(value) || this.deep) && !this.shallow) {
- // set new value
- var oldValue = this.value;
- this.value = value;
- // in debug + async mode, when a watcher callbacks
- // throws, we also throw the saved before-push error
- // so the full cross-tick stack trace is available.
- var prevError = this.prevError;
- /* istanbul ignore if */
- if ('development' !== 'production' && config.debug && prevError) {
- this.prevError = null;
- try {
- this.cb.call(this.vm, value, oldValue);
- } catch (e) {
- nextTick(function () {
- throw prevError;
- }, 0);
- throw e;
- }
- } else {
- this.cb.call(this.vm, value, oldValue);
- }
+/**
+ * Helper that recursively merges two data objects together.
+ */
+function mergeData (to, from) {
+ var key, toVal, fromVal;
+ for (key in from) {
+ toVal = to[key];
+ fromVal = from[key];
+ if (!hasOwn(to, key)) {
+ set(to, key, fromVal);
+ } else if (isObject(toVal) && isObject(fromVal)) {
+ mergeData(toVal, fromVal);
+ }
+ }
+ return to
+}
+
+/**
+ * Data
+ */
+strats.data = function (
+ parentVal,
+ childVal,
+ vm
+) {
+ if (!vm) {
+ // in a Vue.extend merge, both should be functions
+ if (!childVal) {
+ return parentVal
+ }
+ if (typeof childVal !== 'function') {
+ "development" !== 'production' && warn(
+ 'The "data" option should be a function ' +
+ 'that returns a per-instance value in component ' +
+ 'definitions.',
+ vm
+ );
+ return parentVal
+ }
+ if (!parentVal) {
+ return childVal
+ }
+ // when parentVal & childVal are both present,
+ // we need to return a function that returns the
+ // merged result of both functions... no need to
+ // check if parentVal is a function here because
+ // it has to be a function to pass previous merges.
+ return function mergedDataFn () {
+ return mergeData(
+ childVal.call(this),
+ parentVal.call(this)
+ )
+ }
+ } else if (parentVal || childVal) {
+ return function mergedInstanceDataFn () {
+ // instance merge
+ var instanceData = typeof childVal === 'function'
+ ? childVal.call(vm)
+ : childVal;
+ var defaultData = typeof parentVal === 'function'
+ ? parentVal.call(vm)
+ : undefined;
+ if (instanceData) {
+ return mergeData(instanceData, defaultData)
+ } else {
+ return defaultData
}
- this.queued = this.shallow = false;
}
- };
-
- /**
- * Evaluate the value of the watcher.
- * This only gets called for lazy watchers.
- */
-
- Watcher.prototype.evaluate = function () {
- // avoid overwriting another watcher that is being
- // collected.
- var current = Dep.target;
- this.value = this.get();
- this.dirty = false;
- Dep.target = current;
- };
-
- /**
- * Depend on all deps collected by this watcher.
- */
-
- Watcher.prototype.depend = function () {
- var i = this.deps.length;
- while (i--) {
- this.deps[i].depend();
- }
- };
-
- /**
- * Remove self from all dependencies' subcriber list.
- */
+ }
+};
- Watcher.prototype.teardown = function () {
- if (this.active) {
- // remove self from vm's watcher list
- // this is a somewhat expensive operation so we skip it
- // if the vm is being destroyed or is performing a v-for
- // re-render (the watcher list is then filtered by v-for).
- if (!this.vm._isBeingDestroyed && !this.vm._vForRemoving) {
- this.vm._watchers.$remove(this);
+/**
+ * Hooks and param attributes are merged as arrays.
+ */
+function mergeHook (
+ parentVal,
+ childVal
+) {
+ return childVal
+ ? parentVal
+ ? parentVal.concat(childVal)
+ : Array.isArray(childVal)
+ ? childVal
+ : [childVal]
+ : parentVal
+}
+
+config._lifecycleHooks.forEach(function (hook) {
+ strats[hook] = mergeHook;
+});
+
+/**
+ * Assets
+ *
+ * When a vm is present (instance creation), we need to do
+ * a three-way merge between constructor options, instance
+ * options and parent options.
+ */
+function mergeAssets (parentVal, childVal) {
+ var res = Object.create(parentVal || null);
+ return childVal
+ ? extend(res, childVal)
+ : res
+}
+
+config._assetTypes.forEach(function (type) {
+ strats[type + 's'] = mergeAssets;
+});
+
+/**
+ * Watchers.
+ *
+ * Watchers hashes should not overwrite one
+ * another, so we merge them as arrays.
+ */
+strats.watch = function (parentVal, childVal) {
+ /* istanbul ignore if */
+ if (!childVal) { return parentVal }
+ if (!parentVal) { return childVal }
+ var ret = {};
+ extend(ret, parentVal);
+ for (var key in childVal) {
+ var parent = ret[key];
+ var child = childVal[key];
+ if (parent && !Array.isArray(parent)) {
+ parent = [parent];
+ }
+ ret[key] = parent
+ ? parent.concat(child)
+ : [child];
+ }
+ return ret
+};
+
+/**
+ * Other object hashes.
+ */
+strats.props =
+strats.methods =
+strats.computed = function (parentVal, childVal) {
+ if (!childVal) { return parentVal }
+ if (!parentVal) { return childVal }
+ var ret = Object.create(null);
+ extend(ret, parentVal);
+ extend(ret, childVal);
+ return ret
+};
+
+/**
+ * Default strategy.
+ */
+var defaultStrat = function (parentVal, childVal) {
+ return childVal === undefined
+ ? parentVal
+ : childVal
+};
+
+/**
+ * Make sure component options get converted to actual
+ * constructors.
+ */
+function normalizeComponents (options) {
+ if (options.components) {
+ var components = options.components;
+ var def;
+ for (var key in components) {
+ var lower = key.toLowerCase();
+ if (isBuiltInTag(lower) || config.isReservedTag(lower)) {
+ "development" !== 'production' && warn(
+ 'Do not use built-in or reserved HTML elements as component ' +
+ 'id: ' + key
+ );
+ continue
}
- var i = this.deps.length;
- while (i--) {
- this.deps[i].removeSub(this);
+ def = components[key];
+ if (isPlainObject(def)) {
+ components[key] = Vue$3.extend(def);
}
- this.active = false;
- this.vm = this.cb = this.value = null;
}
- };
-
- /**
- * Recrusively traverse an object to evoke all converted
- * getters, so that every nested property inside the object
- * is collected as a "deep" dependency.
- *
- * @param {*} val
- */
+ }
+}
- var seenObjects = new _Set();
- function traverse(val, seen) {
- var i = undefined,
- keys = undefined;
- if (!seen) {
- seen = seenObjects;
- seen.clear();
- }
- var isA = isArray(val);
- var isO = isObject(val);
- if ((isA || isO) && Object.isExtensible(val)) {
- if (val.__ob__) {
- var depId = val.__ob__.dep.id;
- if (seen.has(depId)) {
- return;
- } else {
- seen.add(depId);
- }
- }
- if (isA) {
- i = val.length;
- while (i--) traverse(val[i], seen);
- } else if (isO) {
- keys = Object.keys(val);
- i = keys.length;
- while (i--) traverse(val[keys[i]], seen);
+/**
+ * Ensure all props option syntax are normalized into the
+ * Object-based format.
+ */
+function normalizeProps (options) {
+ var props = options.props;
+ if (!props) { return }
+ var res = {};
+ var i, val, name;
+ if (Array.isArray(props)) {
+ i = props.length;
+ while (i--) {
+ val = props[i];
+ if (typeof val === 'string') {
+ name = camelize(val);
+ res[name] = { type: null };
+ } else {
+ warn('props must be strings when using array syntax.');
}
}
- }
-
- var text$1 = {
-
- bind: function bind() {
- this.attr = this.el.nodeType === 3 ? 'data' : 'textContent';
- },
-
- update: function update(value) {
- this.el[this.attr] = _toString(value);
+ } else if (isPlainObject(props)) {
+ for (var key in props) {
+ val = props[key];
+ name = camelize(key);
+ res[name] = isPlainObject(val)
+ ? val
+ : { type: val };
}
- };
-
- var templateCache = new Cache(1000);
- var idSelectorCache = new Cache(1000);
-
- var map = {
- efault: [0, '', ''],
- legend: [1, '<fieldset>', '</fieldset>'],
- tr: [2, '<table><tbody>', '</tbody></table>'],
- col: [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>']
- };
-
- map.td = map.th = [3, '<table><tbody><tr>', '</tr></tbody></table>'];
-
- map.option = map.optgroup = [1, '<select multiple="multiple">', '</select>'];
-
- map.thead = map.tbody = map.colgroup = map.caption = map.tfoot = [1, '<table>', '</table>'];
-
- map.g = map.defs = map.symbol = map.use = map.image = map.text = map.circle = map.ellipse = map.line = map.path = map.polygon = map.polyline = map.rect = [1, '<svg ' + 'xmlns="http://www.w3.org/2000/svg" ' + 'xmlns:xlink="http://www.w3.org/1999/xlink" ' + 'xmlns:ev="http://www.w3.org/2001/xml-events"' + 'version="1.1">', '</svg>'];
-
- /**
- * Check if a node is a supported template node with a
- * DocumentFragment content.
- *
- * @param {Node} node
- * @return {Boolean}
- */
-
- function isRealTemplate(node) {
- return isTemplate(node) && isFragment(node.content);
}
+ options.props = res;
+}
- var tagRE$1 = /<([\w:-]+)/;
- var entityRE = /&#?\w+?;/;
- var commentRE = /<!--/;
-
- /**
- * Convert a string template to a DocumentFragment.
- * Determines correct wrapping by tag types. Wrapping
- * strategy found in jQuery & component/domify.
- *
- * @param {String} templateString
- * @param {Boolean} raw
- * @return {DocumentFragment}
- */
-
- function stringToFragment(templateString, raw) {
- // try a cache hit first
- var cacheKey = raw ? templateString : templateString.trim();
- var hit = templateCache.get(cacheKey);
- if (hit) {
- return hit;
- }
-
- var frag = document.createDocumentFragment();
- var tagMatch = templateString.match(tagRE$1);
- var entityMatch = entityRE.test(templateString);
- var commentMatch = commentRE.test(templateString);
-
- if (!tagMatch && !entityMatch && !commentMatch) {
- // text only, return a single text node.
- frag.appendChild(document.createTextNode(templateString));
- } else {
- var tag = tagMatch && tagMatch[1];
- var wrap = map[tag] || map.efault;
- var depth = wrap[0];
- var prefix = wrap[1];
- var suffix = wrap[2];
- var node = document.createElement('div');
-
- node.innerHTML = prefix + templateString + suffix;
- while (depth--) {
- node = node.lastChild;
- }
-
- var child;
- /* eslint-disable no-cond-assign */
- while (child = node.firstChild) {
- /* eslint-enable no-cond-assign */
- frag.appendChild(child);
+/**
+ * Normalize raw function directives into object format.
+ */
+function normalizeDirectives (options) {
+ var dirs = options.directives;
+ if (dirs) {
+ for (var key in dirs) {
+ var def = dirs[key];
+ if (typeof def === 'function') {
+ dirs[key] = { bind: def, update: def };
}
}
- if (!raw) {
- trimNode(frag);
- }
- templateCache.put(cacheKey, frag);
- return frag;
}
+}
- /**
- * Convert a template node to a DocumentFragment.
- *
- * @param {Node} node
- * @return {DocumentFragment}
- */
-
- function nodeToFragment(node) {
- // if its a template tag and the browser supports it,
- // its content is already a document fragment. However, iOS Safari has
- // bug when using directly cloned template content with touch
- // events and can cause crashes when the nodes are removed from DOM, so we
- // have to treat template elements as string templates. (#2805)
- /* istanbul ignore if */
- if (isRealTemplate(node)) {
- return stringToFragment(node.innerHTML);
- }
- // script template
- if (node.tagName === 'SCRIPT') {
- return stringToFragment(node.textContent);
- }
- // normal node, clone it to avoid mutating the original
- var clonedNode = cloneNode(node);
- var frag = document.createDocumentFragment();
- var child;
- /* eslint-disable no-cond-assign */
- while (child = clonedNode.firstChild) {
- /* eslint-enable no-cond-assign */
- frag.appendChild(child);
+/**
+ * Merge two option objects into a new one.
+ * Core utility used in both instantiation and inheritance.
+ */
+function mergeOptions (
+ parent,
+ child,
+ vm
+) {
+ normalizeComponents(child);
+ normalizeProps(child);
+ normalizeDirectives(child);
+ var extendsFrom = child.extends;
+ if (extendsFrom) {
+ parent = typeof extendsFrom === 'function'
+ ? mergeOptions(parent, extendsFrom.options, vm)
+ : mergeOptions(parent, extendsFrom, vm);
+ }
+ if (child.mixins) {
+ for (var i = 0, l = child.mixins.length; i < l; i++) {
+ var mixin = child.mixins[i];
+ if (mixin.prototype instanceof Vue$3) {
+ mixin = mixin.options;
+ }
+ parent = mergeOptions(parent, mixin, vm);
+ }
+ }
+ var options = {};
+ var key;
+ for (key in parent) {
+ mergeField(key);
+ }
+ for (key in child) {
+ if (!hasOwn(parent, key)) {
+ mergeField(key);
}
- trimNode(frag);
- return frag;
}
-
- // Test for the presence of the Safari template cloning bug
- // https://bugs.webkit.org/showug.cgi?id=137755
- var hasBrokenTemplate = (function () {
- /* istanbul ignore else */
- if (inBrowser) {
- var a = document.createElement('div');
- a.innerHTML = '<template>1</template>';
- return !a.cloneNode(true).firstChild.innerHTML;
- } else {
- return false;
- }
- })();
-
- // Test for IE10/11 textarea placeholder clone bug
- var hasTextareaCloneBug = (function () {
- /* istanbul ignore else */
- if (inBrowser) {
- var t = document.createElement('textarea');
- t.placeholder = 't';
- return t.cloneNode(true).value === 't';
- } else {
- return false;
- }
- })();
-
- /**
- * 1. Deal with Safari cloning nested <template> bug by
- * manually cloning all template instances.
- * 2. Deal with IE10/11 textarea placeholder bug by setting
- * the correct value after cloning.
- *
- * @param {Element|DocumentFragment} node
- * @return {Element|DocumentFragment}
- */
-
- function cloneNode(node) {
- /* istanbul ignore if */
- if (!node.querySelectorAll) {
- return node.cloneNode();
- }
- var res = node.cloneNode(true);
- var i, original, cloned;
- /* istanbul ignore if */
- if (hasBrokenTemplate) {
- var tempClone = res;
- if (isRealTemplate(node)) {
- node = node.content;
- tempClone = res.content;
- }
- original = node.querySelectorAll('template');
- if (original.length) {
- cloned = tempClone.querySelectorAll('template');
- i = cloned.length;
- while (i--) {
- cloned[i].parentNode.replaceChild(cloneNode(original[i]), cloned[i]);
- }
- }
- }
- /* istanbul ignore if */
- if (hasTextareaCloneBug) {
- if (node.tagName === 'TEXTAREA') {
- res.value = node.value;
- } else {
- original = node.querySelectorAll('textarea');
- if (original.length) {
- cloned = res.querySelectorAll('textarea');
- i = cloned.length;
- while (i--) {
- cloned[i].value = original[i].value;
- }
- }
- }
- }
- return res;
+ function mergeField (key) {
+ var strat = strats[key] || defaultStrat;
+ options[key] = strat(parent[key], child[key], vm, key);
}
+ return options
+}
- /**
- * Process the template option and normalizes it into a
- * a DocumentFragment that can be used as a partial or a
- * instance template.
- *
- * @param {*} template
- * Possible values include:
- * - DocumentFragment object
- * - Node object of type Template
- * - id selector: '#some-template-id'
- * - template string: '<div><span>{{msg}}</span></div>'
- * @param {Boolean} shouldClone
- * @param {Boolean} raw
- * inline HTML interpolation. Do not check for id
- * selector and keep whitespace in the string.
- * @return {DocumentFragment|undefined}
- */
-
- function parseTemplate(template, shouldClone, raw) {
- var node, frag;
-
- // if the template is already a document fragment,
- // do nothing
- if (isFragment(template)) {
- trimNode(template);
- return shouldClone ? cloneNode(template) : template;
- }
-
- if (typeof template === 'string') {
- // id selector
- if (!raw && template.charAt(0) === '#') {
- // id selector can be cached too
- frag = idSelectorCache.get(template);
- if (!frag) {
- node = document.getElementById(template.slice(1));
- if (node) {
- frag = nodeToFragment(node);
- // save selector to cache
- idSelectorCache.put(template, frag);
- }
- }
- } else {
- // normal string template
- frag = stringToFragment(template, raw);
- }
- } else if (template.nodeType) {
- // a direct node
- frag = nodeToFragment(template);
- }
-
- return frag && shouldClone ? cloneNode(frag) : frag;
+/**
+ * Resolve an asset.
+ * This function is used because child instances need access
+ * to assets defined in its ancestor chain.
+ */
+function resolveAsset (
+ options,
+ type,
+ id,
+ warnMissing
+) {
+ /* istanbul ignore if */
+ if (typeof id !== 'string') {
+ return
}
-
-var template = Object.freeze({
- cloneNode: cloneNode,
- parseTemplate: parseTemplate
- });
-
- var html = {
-
- bind: function bind() {
- // a comment node means this is a binding for
- // {{{ inline unescaped html }}}
- if (this.el.nodeType === 8) {
- // hold nodes
- this.nodes = [];
- // replace the placeholder with proper anchor
- this.anchor = createAnchor('v-html');
- replace(this.el, this.anchor);
- }
- },
-
- update: function update(value) {
- value = _toString(value);
- if (this.nodes) {
- this.swap(value);
- } else {
- this.el.innerHTML = value;
- }
- },
-
- swap: function swap(value) {
- // remove old nodes
- var i = this.nodes.length;
- while (i--) {
- remove(this.nodes[i]);
- }
- // convert new value to a fragment
- // do not attempt to retrieve from id selector
- var frag = parseTemplate(value, true, true);
- // save a reference to these nodes so we can remove later
- this.nodes = toArray(frag.childNodes);
- before(frag, this.anchor);
- }
- };
-
- /**
- * Abstraction for a partially-compiled fragment.
- * Can optionally compile content with a child scope.
- *
- * @param {Function} linker
- * @param {Vue} vm
- * @param {DocumentFragment} frag
- * @param {Vue} [host]
- * @param {Object} [scope]
- * @param {Fragment} [parentFrag]
- */
- function Fragment(linker, vm, frag, host, scope, parentFrag) {
- this.children = [];
- this.childFrags = [];
- this.vm = vm;
- this.scope = scope;
- this.inserted = false;
- this.parentFrag = parentFrag;
- if (parentFrag) {
- parentFrag.childFrags.push(this);
- }
- this.unlink = linker(vm, frag, host, scope, this);
- var single = this.single = frag.childNodes.length === 1 &&
- // do not go single mode if the only node is an anchor
- !frag.childNodes[0].__v_anchor;
- if (single) {
- this.node = frag.childNodes[0];
- this.before = singleBefore;
- this.remove = singleRemove;
- } else {
- this.node = createAnchor('fragment-start');
- this.end = createAnchor('fragment-end');
- this.frag = frag;
- prepend(this.node, frag);
- frag.appendChild(this.end);
- this.before = multiBefore;
- this.remove = multiRemove;
- }
- this.node.__v_frag = this;
+ var assets = options[type];
+ var res = assets[id] ||
+ // camelCase ID
+ assets[camelize(id)] ||
+ // Pascal Case ID
+ assets[capitalize(camelize(id))];
+ if ("development" !== 'production' && warnMissing && !res) {
+ warn(
+ 'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
+ options
+ );
+ }
+ return res
+}
+
+/* */
+
+function validateProp (
+ key,
+ propOptions,
+ propsData,
+ vm
+) {
+ var prop = propOptions[key];
+ var absent = !hasOwn(propsData, key);
+ var value = propsData[key];
+ // handle boolean props
+ if (isBooleanType(prop.type)) {
+ if (absent && !hasOwn(prop, 'default')) {
+ value = false;
+ } else if (value === '' || value === hyphenate(key)) {
+ value = true;
+ }
+ }
+ // check default value
+ if (value === undefined) {
+ value = getPropDefaultValue(vm, prop, key);
+ // since the default value is a fresh copy,
+ // make sure to observe it.
+ var prevShouldConvert = observerState.shouldConvert;
+ observerState.shouldConvert = true;
+ observe(value);
+ observerState.shouldConvert = prevShouldConvert;
+ }
+ {
+ assertProp(prop, key, value, vm, absent);
+ }
+ return value
+}
+
+/**
+ * Get the default value of a prop.
+ */
+function getPropDefaultValue (vm, prop, name) {
+ // no default, return undefined
+ if (!hasOwn(prop, 'default')) {
+ return undefined
+ }
+ var def = prop.default;
+ // warn against non-factory defaults for Object & Array
+ if (isObject(def)) {
+ "development" !== 'production' && warn(
+ 'Invalid default value for prop "' + name + '": ' +
+ 'Props with type Object/Array must use a factory function ' +
+ 'to return the default value.',
+ vm
+ );
+ }
+ // call factory function for non-Function types
+ return typeof def === 'function' && prop.type !== Function
+ ? def.call(vm)
+ : def
+}
+
+/**
+ * Assert whether a prop is valid.
+ */
+function assertProp (
+ prop,
+ name,
+ value,
+ vm,
+ absent
+) {
+ if (prop.required && absent) {
+ warn(
+ 'Missing required prop: "' + name + '"',
+ vm
+ );
+ return
+ }
+ if (value == null && !prop.required) {
+ return
+ }
+ var type = prop.type;
+ var valid = !type || type === true;
+ var expectedTypes = [];
+ if (type) {
+ if (!Array.isArray(type)) {
+ type = [type];
+ }
+ for (var i = 0; i < type.length && !valid; i++) {
+ var assertedType = assertType(value, type[i]);
+ expectedTypes.push(assertedType.expectedType);
+ valid = assertedType.valid;
+ }
+ }
+ if (!valid) {
+ warn(
+ 'Invalid prop: type check failed for prop "' + name + '".' +
+ ' Expected ' + expectedTypes.map(capitalize).join(', ') +
+ ', got ' + Object.prototype.toString.call(value).slice(8, -1) + '.',
+ vm
+ );
+ return
+ }
+ var validator = prop.validator;
+ if (validator) {
+ if (!validator(value)) {
+ warn(
+ 'Invalid prop: custom validator check failed for prop "' + name + '".',
+ vm
+ );
+ }
+ }
+}
+
+/**
+ * Assert the type of a value
+ */
+function assertType (value, type) {
+ var valid;
+ var expectedType = getType(type);
+ if (expectedType === 'String') {
+ valid = typeof value === (expectedType = 'string');
+ } else if (expectedType === 'Number') {
+ valid = typeof value === (expectedType = 'number');
+ } else if (expectedType === 'Boolean') {
+ valid = typeof value === (expectedType = 'boolean');
+ } else if (expectedType === 'Function') {
+ valid = typeof value === (expectedType = 'function');
+ } else if (expectedType === 'Object') {
+ valid = isPlainObject(value);
+ } else if (expectedType === 'Array') {
+ valid = Array.isArray(value);
+ } else {
+ valid = value instanceof type;
}
-
- /**
- * Call attach/detach for all components contained within
- * this fragment. Also do so recursively for all child
- * fragments.
- *
- * @param {Function} hook
- */
-
- Fragment.prototype.callHook = function (hook) {
- var i, l;
- for (i = 0, l = this.childFrags.length; i < l; i++) {
- this.childFrags[i].callHook(hook);
- }
- for (i = 0, l = this.children.length; i < l; i++) {
- hook(this.children[i]);
- }
- };
-
- /**
- * Insert fragment before target, single node version
- *
- * @param {Node} target
- * @param {Boolean} withTransition
- */
-
- function singleBefore(target, withTransition) {
- this.inserted = true;
- var method = withTransition !== false ? beforeWithTransition : before;
- method(this.node, target, this.vm);
- if (inDoc(this.node)) {
- this.callHook(attach);
- }
+ return {
+ valid: valid,
+ expectedType: expectedType
}
+}
- /**
- * Remove fragment, single node version
- */
+/**
+ * Use function string name to check built-in types,
+ * because a simple equality check will fail when running
+ * across different vms / iframes.
+ */
+function getType (fn) {
+ var match = fn && fn.toString().match(/^\s*function (\w+)/);
+ return match && match[1]
+}
- function singleRemove() {
- this.inserted = false;
- var shouldCallRemove = inDoc(this.node);
- var self = this;
- this.beforeRemove();
- removeWithTransition(this.node, this.vm, function () {
- if (shouldCallRemove) {
- self.callHook(detach);
- }
- self.destroy();
- });
+function isBooleanType (fn) {
+ if (!Array.isArray(fn)) {
+ return getType(fn) === 'Boolean'
}
-
- /**
- * Insert fragment before target, multi-nodes version
- *
- * @param {Node} target
- * @param {Boolean} withTransition
- */
-
- function multiBefore(target, withTransition) {
- this.inserted = true;
- var vm = this.vm;
- var method = withTransition !== false ? beforeWithTransition : before;
- mapNodeRange(this.node, this.end, function (node) {
- method(node, target, vm);
- });
- if (inDoc(this.node)) {
- this.callHook(attach);
+ for (var i = 0, len = fn.length; i < len; i++) {
+ if (getType(fn[i]) === 'Boolean') {
+ return true
}
}
-
- /**
- * Remove fragment, multi-nodes version
- */
-
- function multiRemove() {
- this.inserted = false;
- var self = this;
- var shouldCallRemove = inDoc(this.node);
- this.beforeRemove();
- removeNodeRange(this.node, this.end, this.vm, this.frag, function () {
- if (shouldCallRemove) {
- self.callHook(detach);
- }
- self.destroy();
- });
- }
-
- /**
- * Prepare the fragment for removal.
- */
-
- Fragment.prototype.beforeRemove = function () {
- var i, l;
- for (i = 0, l = this.childFrags.length; i < l; i++) {
- // call the same method recursively on child
- // fragments, depth-first
- this.childFrags[i].beforeRemove(false);
- }
- for (i = 0, l = this.children.length; i < l; i++) {
- // Call destroy for all contained instances,
- // with remove:false and defer:true.
- // Defer is necessary because we need to
- // keep the children to call detach hooks
- // on them.
- this.children[i].$destroy(false, true);
- }
- var dirs = this.unlink.dirs;
- for (i = 0, l = dirs.length; i < l; i++) {
- // disable the watchers on all the directives
- // so that the rendered content stays the same
- // during removal.
- dirs[i]._watcher && dirs[i]._watcher.teardown();
+ /* istanbul ignore next */
+ return false
+}
+
+
+
+var util = Object.freeze({
+ defineReactive: defineReactive$$1,
+ _toString: _toString,
+ toNumber: toNumber,
+ makeMap: makeMap,
+ isBuiltInTag: isBuiltInTag,
+ remove: remove$1,
+ hasOwn: hasOwn,
+ isPrimitive: isPrimitive,
+ cached: cached,
+ camelize: camelize,
+ capitalize: capitalize,
+ hyphenate: hyphenate,
+ bind: bind$1,
+ toArray: toArray,
+ extend: extend,
+ isObject: isObject,
+ isPlainObject: isPlainObject,
+ toObject: toObject,
+ noop: noop,
+ no: no,
+ genStaticKeys: genStaticKeys,
+ looseEqual: looseEqual,
+ looseIndexOf: looseIndexOf,
+ isReserved: isReserved,
+ def: def,
+ parsePath: parsePath,
+ hasProto: hasProto,
+ inBrowser: inBrowser,
+ UA: UA,
+ isIE: isIE,
+ isIE9: isIE9,
+ isEdge: isEdge,
+ isAndroid: isAndroid,
+ isIOS: isIOS,
+ devtools: devtools,
+ nextTick: nextTick,
+ get _Set () { return _Set; },
+ mergeOptions: mergeOptions,
+ resolveAsset: resolveAsset,
+ get warn () { return warn; },
+ get formatComponentName () { return formatComponentName; },
+ validateProp: validateProp
+});
+
+/* */
+
+function initUse (Vue) {
+ Vue.use = function (plugin) {
+ /* istanbul ignore if */
+ if (plugin.installed) {
+ return
}
+ // additional parameters
+ var args = toArray(arguments, 1);
+ args.unshift(this);
+ if (typeof plugin.install === 'function') {
+ plugin.install.apply(plugin, args);
+ } else {
+ plugin.apply(null, args);
+ }
+ plugin.installed = true;
+ return this
};
+}
- /**
- * Destroy the fragment.
- */
+/* */
- Fragment.prototype.destroy = function () {
- if (this.parentFrag) {
- this.parentFrag.childFrags.$remove(this);
- }
- this.node.__v_frag = null;
- this.unlink();
+function initMixin$1 (Vue) {
+ Vue.mixin = function (mixin) {
+ Vue.options = mergeOptions(Vue.options, mixin);
};
+}
- /**
- * Call attach hook for a Vue instance.
- *
- * @param {Vue} child
- */
-
- function attach(child) {
- if (!child._isAttached && inDoc(child.$el)) {
- child._callHook('attached');
- }
- }
+/* */
+function initExtend (Vue) {
/**
- * Call detach hook for a Vue instance.
- *
- * @param {Vue} child
+ * Each instance constructor, including Vue, has a unique
+ * cid. This enables us to create wrapped "child
+ * constructors" for prototypal inheritance and cache them.
*/
-
- function detach(child) {
- if (child._isAttached && !inDoc(child.$el)) {
- child._callHook('detached');
- }
- }
-
- var linkerCache = new Cache(5000);
+ Vue.cid = 0;
+ var cid = 1;
/**
- * A factory that can be used to create instances of a
- * fragment. Caches the compiled linker if possible.
- *
- * @param {Vue} vm
- * @param {Element|String} el
+ * Class inheritance
*/
- function FragmentFactory(vm, el) {
- this.vm = vm;
- var template;
- var isString = typeof el === 'string';
- if (isString || isTemplate(el) && !el.hasAttribute('v-if')) {
- template = parseTemplate(el, true);
- } else {
- template = document.createDocumentFragment();
- template.appendChild(el);
- }
- this.template = template;
- // linker can be cached, but only for components
- var linker;
- var cid = vm.constructor.cid;
- if (cid > 0) {
- var cacheId = cid + (isString ? el : getOuterHTML(el));
- linker = linkerCache.get(cacheId);
- if (!linker) {
- linker = compile(template, vm.$options, true);
- linkerCache.put(cacheId, linker);
+ Vue.extend = function (extendOptions) {
+ extendOptions = extendOptions || {};
+ var Super = this;
+ var isFirstExtend = Super.cid === 0;
+ if (isFirstExtend && extendOptions._Ctor) {
+ return extendOptions._Ctor
+ }
+ var name = extendOptions.name || Super.options.name;
+ {
+ if (!/^[a-zA-Z][\w-]*$/.test(name)) {
+ warn(
+ 'Invalid component name: "' + name + '". Component names ' +
+ 'can only contain alphanumeric characaters and the hyphen.'
+ );
+ name = null;
}
- } else {
- linker = compile(template, vm.$options, true);
}
- this.linker = linker;
- }
-
- /**
- * Create a fragment instance with given host and scope.
- *
- * @param {Vue} host
- * @param {Object} scope
- * @param {Fragment} parentFrag
- */
-
- FragmentFactory.prototype.create = function (host, scope, parentFrag) {
- var frag = cloneNode(this.template);
- return new Fragment(this.linker, this.vm, frag, host, scope, parentFrag);
+ var Sub = function VueComponent (options) {
+ this._init(options);
+ };
+ Sub.prototype = Object.create(Super.prototype);
+ Sub.prototype.constructor = Sub;
+ Sub.cid = cid++;
+ Sub.options = mergeOptions(
+ Super.options,
+ extendOptions
+ );
+ Sub['super'] = Super;
+ // allow further extension
+ Sub.extend = Super.extend;
+ // create asset registers, so extended classes
+ // can have their private assets too.
+ config._assetTypes.forEach(function (type) {
+ Sub[type] = Super[type];
+ });
+ // enable recursive self-lookup
+ if (name) {
+ Sub.options.components[name] = Sub;
+ }
+ // keep a reference to the super options at extension time.
+ // later at instantiation we can check if Super's options have
+ // been updated.
+ Sub.superOptions = Super.options;
+ Sub.extendOptions = extendOptions;
+ // cache constructor
+ if (isFirstExtend) {
+ extendOptions._Ctor = Sub;
+ }
+ return Sub
};
+}
- var ON = 700;
- var MODEL = 800;
- var BIND = 850;
- var TRANSITION = 1100;
- var EL = 1500;
- var COMPONENT = 1500;
- var PARTIAL = 1750;
- var IF = 2100;
- var FOR = 2200;
- var SLOT = 2300;
-
- var uid$3 = 0;
-
- var vFor = {
-
- priority: FOR,
- terminal: true,
-
- params: ['track-by', 'stagger', 'enter-stagger', 'leave-stagger'],
-
- bind: function bind() {
- // support "item in/of items" syntax
- var inMatch = this.expression.match(/(.*) (?:in|of) (.*)/);
- if (inMatch) {
- var itMatch = inMatch[1].match(/\((.*),(.*)\)/);
- if (itMatch) {
- this.iterator = itMatch[1].trim();
- this.alias = itMatch[2].trim();
- } else {
- this.alias = inMatch[1].trim();
- }
- this.expression = inMatch[2];
- }
-
- if (!this.alias) {
- 'development' !== 'production' && warn('Invalid v-for expression "' + this.descriptor.raw + '": ' + 'alias is required.', this.vm);
- return;
- }
-
- // uid as a cache identifier
- this.id = '__v-for__' + ++uid$3;
-
- // check if this is an option list,
- // so that we know if we need to update the <select>'s
- // v-model when the option list has changed.
- // because v-model has a lower priority than v-for,
- // the v-model is not bound here yet, so we have to
- // retrive it in the actual updateModel() function.
- var tag = this.el.tagName;
- this.isOption = (tag === 'OPTION' || tag === 'OPTGROUP') && this.el.parentNode.tagName === 'SELECT';
-
- // setup anchor nodes
- this.start = createAnchor('v-for-start');
- this.end = createAnchor('v-for-end');
- replace(this.el, this.end);
- before(this.start, this.end);
-
- // cache
- this.cache = Object.create(null);
-
- // fragment factory
- this.factory = new FragmentFactory(this.vm, this.el);
- },
-
- update: function update(data) {
- this.diff(data);
- this.updateRef();
- this.updateModel();
- },
-
- /**
- * Diff, based on new data and old data, determine the
- * minimum amount of DOM manipulations needed to make the
- * DOM reflect the new data Array.
- *
- * The algorithm diffs the new data Array by storing a
- * hidden reference to an owner vm instance on previously
- * seen data. This allows us to achieve O(n) which is
- * better than a levenshtein distance based algorithm,
- * which is O(m * n).
- *
- * @param {Array} data
- */
-
- diff: function diff(data) {
- // check if the Array was converted from an Object
- var item = data[0];
- var convertedFromObject = this.fromObject = isObject(item) && hasOwn(item, '$key') && hasOwn(item, '$value');
-
- var trackByKey = this.params.trackBy;
- var oldFrags = this.frags;
- var frags = this.frags = new Array(data.length);
- var alias = this.alias;
- var iterator = this.iterator;
- var start = this.start;
- var end = this.end;
- var inDocument = inDoc(start);
- var init = !oldFrags;
- var i, l, frag, key, value, primitive;
-
- // First pass, go through the new Array and fill up
- // the new frags array. If a piece of data has a cached
- // instance for it, we reuse it. Otherwise build a new
- // instance.
- for (i = 0, l = data.length; i < l; i++) {
- item = data[i];
- key = convertedFromObject ? item.$key : null;
- value = convertedFromObject ? item.$value : item;
- primitive = !isObject(value);
- frag = !init && this.getCachedFrag(value, i, key);
- if (frag) {
- // reusable fragment
- frag.reused = true;
- // update $index
- frag.scope.$index = i;
- // update $key
- if (key) {
- frag.scope.$key = key;
- }
- // update iterator
- if (iterator) {
- frag.scope[iterator] = key !== null ? key : i;
- }
- // update data for track-by, object repeat &
- // primitive values.
- if (trackByKey || convertedFromObject || primitive) {
- withoutConversion(function () {
- frag.scope[alias] = value;
- });
- }
- } else {
- // new isntance
- frag = this.create(value, alias, i, key);
- frag.fresh = !init;
- }
- frags[i] = frag;
- if (init) {
- frag.before(end);
- }
- }
-
- // we're done for the initial render.
- if (init) {
- return;
- }
-
- // Second pass, go through the old fragments and
- // destroy those who are not reused (and remove them
- // from cache)
- var removalIndex = 0;
- var totalRemoved = oldFrags.length - frags.length;
- // when removing a large number of fragments, watcher removal
- // turns out to be a perf bottleneck, so we batch the watcher
- // removals into a single filter call!
- this.vm._vForRemoving = true;
- for (i = 0, l = oldFrags.length; i < l; i++) {
- frag = oldFrags[i];
- if (!frag.reused) {
- this.deleteCachedFrag(frag);
- this.remove(frag, removalIndex++, totalRemoved, inDocument);
- }
- }
- this.vm._vForRemoving = false;
- if (removalIndex) {
- this.vm._watchers = this.vm._watchers.filter(function (w) {
- return w.active;
- });
- }
-
- // Final pass, move/insert new fragments into the
- // right place.
- var targetPrev, prevEl, currentPrev;
- var insertionIndex = 0;
- for (i = 0, l = frags.length; i < l; i++) {
- frag = frags[i];
- // this is the frag that we should be after
- targetPrev = frags[i - 1];
- prevEl = targetPrev ? targetPrev.staggerCb ? targetPrev.staggerAnchor : targetPrev.end || targetPrev.node : start;
- if (frag.reused && !frag.staggerCb) {
- currentPrev = findPrevFrag(frag, start, this.id);
- if (currentPrev !== targetPrev && (!currentPrev ||
- // optimization for moving a single item.
- // thanks to suggestions by @livoras in #1807
- findPrevFrag(currentPrev, start, this.id) !== targetPrev)) {
- this.move(frag, prevEl);
- }
- } else {
- // new instance, or still in stagger.
- // insert with updated stagger index.
- this.insert(frag, insertionIndex++, prevEl, inDocument);
- }
- frag.reused = frag.fresh = false;
- }
- },
-
- /**
- * Create a new fragment instance.
- *
- * @param {*} value
- * @param {String} alias
- * @param {Number} index
- * @param {String} [key]
- * @return {Fragment}
- */
-
- create: function create(value, alias, index, key) {
- var host = this._host;
- // create iteration scope
- var parentScope = this._scope || this.vm;
- var scope = Object.create(parentScope);
- // ref holder for the scope
- scope.$refs = Object.create(parentScope.$refs);
- scope.$els = Object.create(parentScope.$els);
- // make sure point $parent to parent scope
- scope.$parent = parentScope;
- // for two-way binding on alias
- scope.$forContext = this;
- // define scope properties
- // important: define the scope alias without forced conversion
- // so that frozen data structures remain non-reactive.
- withoutConversion(function () {
- defineReactive(scope, alias, value);
- });
- defineReactive(scope, '$index', index);
- if (key) {
- defineReactive(scope, '$key', key);
- } else if (scope.$key) {
- // avoid accidental fallback
- def(scope, '$key', null);
- }
- if (this.iterator) {
- defineReactive(scope, this.iterator, key !== null ? key : index);
- }
- var frag = this.factory.create(host, scope, this._frag);
- frag.forId = this.id;
- this.cacheFrag(value, frag, index, key);
- return frag;
- },
-
- /**
- * Update the v-ref on owner vm.
- */
-
- updateRef: function updateRef() {
- var ref = this.descriptor.ref;
- if (!ref) return;
- var hash = (this._scope || this.vm).$refs;
- var refs;
- if (!this.fromObject) {
- refs = this.frags.map(findVmFromFrag);
- } else {
- refs = {};
- this.frags.forEach(function (frag) {
- refs[frag.scope.$key] = findVmFromFrag(frag);
- });
- }
- hash[ref] = refs;
- },
-
- /**
- * For option lists, update the containing v-model on
- * parent <select>.
- */
-
- updateModel: function updateModel() {
- if (this.isOption) {
- var parent = this.start.parentNode;
- var model = parent && parent.__v_model;
- if (model) {
- model.forceUpdate();
- }
- }
- },
+/* */
- /**
- * Insert a fragment. Handles staggering.
- *
- * @param {Fragment} frag
- * @param {Number} index
- * @param {Node} prevEl
- * @param {Boolean} inDocument
- */
-
- insert: function insert(frag, index, prevEl, inDocument) {
- if (frag.staggerCb) {
- frag.staggerCb.cancel();
- frag.staggerCb = null;
- }
- var staggerAmount = this.getStagger(frag, index, null, 'enter');
- if (inDocument && staggerAmount) {
- // create an anchor and insert it synchronously,
- // so that we can resolve the correct order without
- // worrying about some elements not inserted yet
- var anchor = frag.staggerAnchor;
- if (!anchor) {
- anchor = frag.staggerAnchor = createAnchor('stagger-anchor');
- anchor.__v_frag = frag;
- }
- after(anchor, prevEl);
- var op = frag.staggerCb = cancellable(function () {
- frag.staggerCb = null;
- frag.before(anchor);
- remove(anchor);
- });
- setTimeout(op, staggerAmount);
+function initAssetRegisters (Vue) {
+ /**
+ * Create asset registration methods.
+ */
+ config._assetTypes.forEach(function (type) {
+ Vue[type] = function (
+ id,
+ definition
+ ) {
+ if (!definition) {
+ return this.options[type + 's'][id]
} else {
- var target = prevEl.nextSibling;
/* istanbul ignore if */
- if (!target) {
- // reset end anchor position in case the position was messed up
- // by an external drag-n-drop library.
- after(this.end, prevEl);
- target = this.end;
- }
- frag.before(target);
- }
- },
-
- /**
- * Remove a fragment. Handles staggering.
- *
- * @param {Fragment} frag
- * @param {Number} index
- * @param {Number} total
- * @param {Boolean} inDocument
- */
-
- remove: function remove(frag, index, total, inDocument) {
- if (frag.staggerCb) {
- frag.staggerCb.cancel();
- frag.staggerCb = null;
- // it's not possible for the same frag to be removed
- // twice, so if we have a pending stagger callback,
- // it means this frag is queued for enter but removed
- // before its transition started. Since it is already
- // destroyed, we can just leave it in detached state.
- return;
- }
- var staggerAmount = this.getStagger(frag, index, total, 'leave');
- if (inDocument && staggerAmount) {
- var op = frag.staggerCb = cancellable(function () {
- frag.staggerCb = null;
- frag.remove();
- });
- setTimeout(op, staggerAmount);
- } else {
- frag.remove();
- }
- },
-
- /**
- * Move a fragment to a new position.
- * Force no transition.
- *
- * @param {Fragment} frag
- * @param {Node} prevEl
- */
-
- move: function move(frag, prevEl) {
- // fix a common issue with Sortable:
- // if prevEl doesn't have nextSibling, this means it's
- // been dragged after the end anchor. Just re-position
- // the end anchor to the end of the container.
- /* istanbul ignore if */
- if (!prevEl.nextSibling) {
- this.end.parentNode.appendChild(this.end);
- }
- frag.before(prevEl.nextSibling, false);
- },
-
- /**
- * Cache a fragment using track-by or the object key.
- *
- * @param {*} value
- * @param {Fragment} frag
- * @param {Number} index
- * @param {String} [key]
- */
-
- cacheFrag: function cacheFrag(value, frag, index, key) {
- var trackByKey = this.params.trackBy;
- var cache = this.cache;
- var primitive = !isObject(value);
- var id;
- if (key || trackByKey || primitive) {
- id = getTrackByKey(index, key, value, trackByKey);
- if (!cache[id]) {
- cache[id] = frag;
- } else if (trackByKey !== '$index') {
- 'development' !== 'production' && this.warnDuplicate(value);
- }
- } else {
- id = this.id;
- if (hasOwn(value, id)) {
- if (value[id] === null) {
- value[id] = frag;
- } else {
- 'development' !== 'production' && this.warnDuplicate(value);
+ {
+ if (type === 'component' && config.isReservedTag(id)) {
+ warn(
+ 'Do not use built-in or reserved HTML elements as component ' +
+ 'id: ' + id
+ );
}
- } else if (Object.isExtensible(value)) {
- def(value, id, frag);
- } else if ('development' !== 'production') {
- warn('Frozen v-for objects cannot be automatically tracked, make sure to ' + 'provide a track-by key.');
- }
- }
- frag.raw = value;
- },
-
- /**
- * Get a cached fragment from the value/index/key
- *
- * @param {*} value
- * @param {Number} index
- * @param {String} key
- * @return {Fragment}
- */
-
- getCachedFrag: function getCachedFrag(value, index, key) {
- var trackByKey = this.params.trackBy;
- var primitive = !isObject(value);
- var frag;
- if (key || trackByKey || primitive) {
- var id = getTrackByKey(index, key, value, trackByKey);
- frag = this.cache[id];
- } else {
- frag = value[this.id];
- }
- if (frag && (frag.reused || frag.fresh)) {
- 'development' !== 'production' && this.warnDuplicate(value);
- }
- return frag;
- },
-
- /**
- * Delete a fragment from cache.
- *
- * @param {Fragment} frag
- */
-
- deleteCachedFrag: function deleteCachedFrag(frag) {
- var value = frag.raw;
- var trackByKey = this.params.trackBy;
- var scope = frag.scope;
- var index = scope.$index;
- // fix #948: avoid accidentally fall through to
- // a parent repeater which happens to have $key.
- var key = hasOwn(scope, '$key') && scope.$key;
- var primitive = !isObject(value);
- if (trackByKey || key || primitive) {
- var id = getTrackByKey(index, key, value, trackByKey);
- this.cache[id] = null;
- } else {
- value[this.id] = null;
- frag.raw = null;
- }
- },
-
- /**
- * Get the stagger amount for an insertion/removal.
- *
- * @param {Fragment} frag
- * @param {Number} index
- * @param {Number} total
- * @param {String} type
- */
-
- getStagger: function getStagger(frag, index, total, type) {
- type = type + 'Stagger';
- var trans = frag.node.__v_trans;
- var hooks = trans && trans.hooks;
- var hook = hooks && (hooks[type] || hooks.stagger);
- return hook ? hook.call(frag, index, total) : index * parseInt(this.params[type] || this.params.stagger, 10);
- },
-
- /**
- * Pre-process the value before piping it through the
- * filters. This is passed to and called by the watcher.
- */
-
- _preProcess: function _preProcess(value) {
- // regardless of type, store the un-filtered raw value.
- this.rawValue = value;
- return value;
- },
-
- /**
- * Post-process the value after it has been piped through
- * the filters. This is passed to and called by the watcher.
- *
- * It is necessary for this to be called during the
- * watcher's dependency collection phase because we want
- * the v-for to update when the source Object is mutated.
- */
-
- _postProcess: function _postProcess(value) {
- if (isArray(value)) {
- return value;
- } else if (isPlainObject(value)) {
- // convert plain object to array.
- var keys = Object.keys(value);
- var i = keys.length;
- var res = new Array(i);
- var key;
- while (i--) {
- key = keys[i];
- res[i] = {
- $key: key,
- $value: value[key]
- };
}
- return res;
- } else {
- if (typeof value === 'number' && !isNaN(value)) {
- value = range(value);
+ if (type === 'component' && isPlainObject(definition)) {
+ definition.name = definition.name || id;
+ definition = Vue.extend(definition);
}
- return value || [];
- }
- },
-
- unbind: function unbind() {
- if (this.descriptor.ref) {
- (this._scope || this.vm).$refs[this.descriptor.ref] = null;
- }
- if (this.frags) {
- var i = this.frags.length;
- var frag;
- while (i--) {
- frag = this.frags[i];
- this.deleteCachedFrag(frag);
- frag.destroy();
+ if (type === 'directive' && typeof definition === 'function') {
+ definition = { bind: definition, update: definition };
}
+ this.options[type + 's'][id] = definition;
+ return definition
}
- }
- };
-
- /**
- * Helper to find the previous element that is a fragment
- * anchor. This is necessary because a destroyed frag's
- * element could still be lingering in the DOM before its
- * leaving transition finishes, but its inserted flag
- * should have been set to false so we can skip them.
- *
- * If this is a block repeat, we want to make sure we only
- * return frag that is bound to this v-for. (see #929)
- *
- * @param {Fragment} frag
- * @param {Comment|Text} anchor
- * @param {String} id
- * @return {Fragment}
- */
-
- function findPrevFrag(frag, anchor, id) {
- var el = frag.node.previousSibling;
- /* istanbul ignore if */
- if (!el) return;
- frag = el.__v_frag;
- while ((!frag || frag.forId !== id || !frag.inserted) && el !== anchor) {
- el = el.previousSibling;
- /* istanbul ignore if */
- if (!el) return;
- frag = el.__v_frag;
- }
- return frag;
- }
-
- /**
- * Find a vm from a fragment.
- *
- * @param {Fragment} frag
- * @return {Vue|undefined}
- */
-
- function findVmFromFrag(frag) {
- var node = frag.node;
- // handle multi-node frag
- if (frag.end) {
- while (!node.__vue__ && node !== frag.end && node.nextSibling) {
- node = node.nextSibling;
+ };
+ });
+}
+
+var KeepAlive = {
+ name: 'keep-alive',
+ abstract: true,
+ created: function created () {
+ this.cache = Object.create(null);
+ },
+ render: function render () {
+ var vnode = getFirstComponentChild(this.$slots.default);
+ if (vnode && vnode.componentOptions) {
+ var opts = vnode.componentOptions;
+ var key = vnode.key == null
+ // same constructor may get registered as different local components
+ // so cid alone is not enough (#3269)
+ ? opts.Ctor.cid + '::' + opts.tag
+ : vnode.key;
+ if (this.cache[key]) {
+ vnode.child = this.cache[key].child;
+ } else {
+ this.cache[key] = vnode;
}
+ vnode.data.keepAlive = true;
}
- return node.__vue__;
- }
-
- /**
- * Create a range array from given number.
- *
- * @param {Number} n
- * @return {Array}
- */
+ return vnode
+ },
+ destroyed: function destroyed () {
+ var this$1 = this;
- function range(n) {
- var i = -1;
- var ret = new Array(Math.floor(n));
- while (++i < n) {
- ret[i] = i;
+ for (var key in this.cache) {
+ var vnode = this$1.cache[key];
+ callHook(vnode.child, 'deactivated');
+ vnode.child.$destroy();
}
- return ret;
}
+};
- /**
- * Get the track by key for an item.
- *
- * @param {Number} index
- * @param {String} key
- * @param {*} value
- * @param {String} [trackByKey]
- */
+var builtInComponents = {
+ KeepAlive: KeepAlive
+};
- function getTrackByKey(index, key, value, trackByKey) {
- return trackByKey ? trackByKey === '$index' ? index : trackByKey.charAt(0).match(/\w/) ? getPath(value, trackByKey) : value[trackByKey] : key || value;
- }
+/* */
- if ('development' !== 'production') {
- vFor.warnDuplicate = function (value) {
- warn('Duplicate value found in v-for="' + this.descriptor.raw + '": ' + JSON.stringify(value) + '. Use track-by="$index" if ' + 'you are expecting duplicate values.', this.vm);
+function initGlobalAPI (Vue) {
+ // config
+ var configDef = {};
+ configDef.get = function () { return config; };
+ {
+ configDef.set = function () {
+ warn(
+ 'Do not replace the Vue.config object, set individual fields instead.'
+ );
};
}
+ Object.defineProperty(Vue, 'config', configDef);
+ Vue.util = util;
+ Vue.set = set;
+ Vue.delete = del;
+ Vue.nextTick = nextTick;
- var vIf = {
+ Vue.options = Object.create(null);
+ config._assetTypes.forEach(function (type) {
+ Vue.options[type + 's'] = Object.create(null);
+ });
- priority: IF,
- terminal: true,
+ extend(Vue.options.components, builtInComponents);
- bind: function bind() {
- var el = this.el;
- if (!el.__vue__) {
- // check else block
- var next = el.nextElementSibling;
- if (next && getAttr(next, 'v-else') !== null) {
- remove(next);
- this.elseEl = next;
- }
- // check main block
- this.anchor = createAnchor('v-if');
- replace(el, this.anchor);
- } else {
- 'development' !== 'production' && warn('v-if="' + this.expression + '" cannot be ' + 'used on an instance root element.', this.vm);
- this.invalid = true;
- }
- },
+ initUse(Vue);
+ initMixin$1(Vue);
+ initExtend(Vue);
+ initAssetRegisters(Vue);
+}
- update: function update(value) {
- if (this.invalid) return;
- if (value) {
- if (!this.frag) {
- this.insert();
- }
- } else {
- this.remove();
- }
- },
+initGlobalAPI(Vue$3);
- insert: function insert() {
- if (this.elseFrag) {
- this.elseFrag.remove();
- this.elseFrag = null;
- }
- // lazy init factory
- if (!this.factory) {
- this.factory = new FragmentFactory(this.vm, this.el);
- }
- this.frag = this.factory.create(this._host, this._scope, this._frag);
- this.frag.before(this.anchor);
- },
+Object.defineProperty(Vue$3.prototype, '$isServer', {
+ get: function () { return config._isServer; }
+});
- remove: function remove() {
- if (this.frag) {
- this.frag.remove();
- this.frag = null;
- }
- if (this.elseEl && !this.elseFrag) {
- if (!this.elseFactory) {
- this.elseFactory = new FragmentFactory(this.elseEl._context || this.vm, this.elseEl);
- }
- this.elseFrag = this.elseFactory.create(this._host, this._scope, this._frag);
- this.elseFrag.before(this.anchor);
- }
- },
+Vue$3.version = '2.0.3';
- unbind: function unbind() {
- if (this.frag) {
- this.frag.destroy();
- }
- if (this.elseFrag) {
- this.elseFrag.destroy();
- }
- }
- };
+/* */
- var show = {
+// attributes that should be using props for binding
+var mustUseProp = makeMap('value,selected,checked,muted');
- bind: function bind() {
- // check else block
- var next = this.el.nextElementSibling;
- if (next && getAttr(next, 'v-else') !== null) {
- this.elseEl = next;
- }
- },
+var isEnumeratedAttr = makeMap('contenteditable,draggable,spellcheck');
- update: function update(value) {
- this.apply(this.el, value);
- if (this.elseEl) {
- this.apply(this.elseEl, !value);
- }
- },
+var isBooleanAttr = makeMap(
+ 'allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,' +
+ 'default,defaultchecked,defaultmuted,defaultselected,defer,disabled,' +
+ 'enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,' +
+ 'muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,' +
+ 'required,reversed,scoped,seamless,selected,sortable,translate,' +
+ 'truespeed,typemustmatch,visible'
+);
- apply: function apply(el, value) {
- if (inDoc(el)) {
- applyTransition(el, value ? 1 : -1, toggle, this.vm);
- } else {
- toggle();
- }
- function toggle() {
- el.style.display = value ? '' : 'none';
- }
- }
- };
+var isAttr = makeMap(
+ 'accept,accept-charset,accesskey,action,align,alt,async,autocomplete,' +
+ 'autofocus,autoplay,autosave,bgcolor,border,buffered,challenge,charset,' +
+ 'checked,cite,class,code,codebase,color,cols,colspan,content,http-equiv,' +
+ 'name,contenteditable,contextmenu,controls,coords,data,datetime,default,' +
+ 'defer,dir,dirname,disabled,download,draggable,dropzone,enctype,method,for,' +
+ 'form,formaction,headers,<th>,height,hidden,high,href,hreflang,http-equiv,' +
+ 'icon,id,ismap,itemprop,keytype,kind,label,lang,language,list,loop,low,' +
+ 'manifest,max,maxlength,media,method,GET,POST,min,multiple,email,file,' +
+ 'muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,' +
+ 'preload,radiogroup,readonly,rel,required,reversed,rows,rowspan,sandbox,' +
+ 'scope,scoped,seamless,selected,shape,size,type,text,password,sizes,span,' +
+ 'spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,' +
+ 'target,title,type,usemap,value,width,wrap'
+);
- var text$2 = {
-
- bind: function bind() {
- var self = this;
- var el = this.el;
- var isRange = el.type === 'range';
- var lazy = this.params.lazy;
- var number = this.params.number;
- var debounce = this.params.debounce;
-
- // handle composition events.
- // http://blog.evanyou.me/2014/01/03/composition-event/
- // skip this for Android because it handles composition
- // events quite differently. Android doesn't trigger
- // composition events for language input methods e.g.
- // Chinese, but instead triggers them for spelling
- // suggestions... (see Discussion/#162)
- var composing = false;
- if (!isAndroid && !isRange) {
- this.on('compositionstart', function () {
- composing = true;
- });
- this.on('compositionend', function () {
- composing = false;
- // in IE11 the "compositionend" event fires AFTER
- // the "input" event, so the input handler is blocked
- // at the end... have to call it here.
- //
- // #1327: in lazy mode this is unecessary.
- if (!lazy) {
- self.listener();
- }
- });
- }
- // prevent messing with the input when user is typing,
- // and force update on blur.
- this.focused = false;
- if (!isRange && !lazy) {
- this.on('focus', function () {
- self.focused = true;
- });
- this.on('blur', function () {
- self.focused = false;
- // do not sync value after fragment removal (#2017)
- if (!self._frag || self._frag.inserted) {
- self.rawListener();
- }
- });
- }
- // Now attach the main listener
- this.listener = this.rawListener = function () {
- if (composing || !self._bound) {
- return;
- }
- var val = number || isRange ? toNumber(el.value) : el.value;
- self.set(val);
- // force update on next tick to avoid lock & same value
- // also only update when user is not typing
- nextTick(function () {
- if (self._bound && !self.focused) {
- self.update(self._watcher.value);
- }
- });
- };
+var xlinkNS = 'http://www.w3.org/1999/xlink';
- // apply debounce
- if (debounce) {
- this.listener = _debounce(this.listener, debounce);
- }
-
- // Support jQuery events, since jQuery.trigger() doesn't
- // trigger native events in some cases and some plugins
- // rely on $.trigger()
- //
- // We want to make sure if a listener is attached using
- // jQuery, it is also removed with jQuery, that's why
- // we do the check for each directive instance and
- // store that check result on itself. This also allows
- // easier test coverage control by unsetting the global
- // jQuery variable in tests.
- this.hasjQuery = typeof jQuery === 'function';
- if (this.hasjQuery) {
- var method = jQuery.fn.on ? 'on' : 'bind';
- jQuery(el)[method]('change', this.rawListener);
- if (!lazy) {
- jQuery(el)[method]('input', this.listener);
- }
- } else {
- this.on('change', this.rawListener);
- if (!lazy) {
- this.on('input', this.listener);
- }
- }
+var isXlink = function (name) {
+ return name.charAt(5) === ':' && name.slice(0, 5) === 'xlink'
+};
- // IE9 doesn't fire input event on backspace/del/cut
- if (!lazy && isIE9) {
- this.on('cut', function () {
- nextTick(self.listener);
- });
- this.on('keyup', function (e) {
- if (e.keyCode === 46 || e.keyCode === 8) {
- self.listener();
- }
- });
- }
+var getXlinkProp = function (name) {
+ return isXlink(name) ? name.slice(6, name.length) : ''
+};
- // set initial value if present
- if (el.hasAttribute('value') || el.tagName === 'TEXTAREA' && el.value.trim()) {
- this.afterBind = this.listener;
- }
- },
+var isFalsyAttrValue = function (val) {
+ return val == null || val === false
+};
- update: function update(value) {
- // #3029 only update when the value changes. This prevent
- // browsers from overwriting values like selectionStart
- value = _toString(value);
- if (value !== this.el.value) this.el.value = value;
- },
+/* */
- unbind: function unbind() {
- var el = this.el;
- if (this.hasjQuery) {
- var method = jQuery.fn.off ? 'off' : 'unbind';
- jQuery(el)[method]('change', this.listener);
- jQuery(el)[method]('input', this.listener);
- }
+function genClassForVnode (vnode) {
+ var data = vnode.data;
+ var parentNode = vnode;
+ var childNode = vnode;
+ while (childNode.child) {
+ childNode = childNode.child._vnode;
+ if (childNode.data) {
+ data = mergeClassData(childNode.data, data);
}
- };
-
- var radio = {
-
- bind: function bind() {
- var self = this;
- var el = this.el;
-
- this.getValue = function () {
- // value overwrite via v-bind:value
- if (el.hasOwnProperty('_value')) {
- return el._value;
- }
- var val = el.value;
- if (self.params.number) {
- val = toNumber(val);
- }
- return val;
- };
-
- this.listener = function () {
- self.set(self.getValue());
- };
- this.on('change', this.listener);
-
- if (el.hasAttribute('checked')) {
- this.afterBind = this.listener;
- }
- },
-
- update: function update(value) {
- this.el.checked = looseEqual(value, this.getValue());
+ }
+ while ((parentNode = parentNode.parent)) {
+ if (parentNode.data) {
+ data = mergeClassData(data, parentNode.data);
}
- };
+ }
+ return genClassFromData(data)
+}
- var select = {
+function mergeClassData (child, parent) {
+ return {
+ staticClass: concat(child.staticClass, parent.staticClass),
+ class: child.class
+ ? [child.class, parent.class]
+ : parent.class
+ }
+}
- bind: function bind() {
- var _this = this;
+function genClassFromData (data) {
+ var dynamicClass = data.class;
+ var staticClass = data.staticClass;
+ if (staticClass || dynamicClass) {
+ return concat(staticClass, stringifyClass(dynamicClass))
+ }
+ /* istanbul ignore next */
+ return ''
+}
- var self = this;
- var el = this.el;
+function concat (a, b) {
+ return a ? b ? (a + ' ' + b) : a : (b || '')
+}
- // method to force update DOM using latest value.
- this.forceUpdate = function () {
- if (self._watcher) {
- self.update(self._watcher.get());
+function stringifyClass (value) {
+ var res = '';
+ if (!value) {
+ return res
+ }
+ if (typeof value === 'string') {
+ return value
+ }
+ if (Array.isArray(value)) {
+ var stringified;
+ for (var i = 0, l = value.length; i < l; i++) {
+ if (value[i]) {
+ if ((stringified = stringifyClass(value[i]))) {
+ res += stringified + ' ';
}
- };
-
- // check if this is a multiple select
- var multiple = this.multiple = el.hasAttribute('multiple');
-
- // attach listener
- this.listener = function () {
- var value = getValue(el, multiple);
- value = self.params.number ? isArray(value) ? value.map(toNumber) : toNumber(value) : value;
- self.set(value);
- };
- this.on('change', this.listener);
-
- // if has initial value, set afterBind
- var initValue = getValue(el, multiple, true);
- if (multiple && initValue.length || !multiple && initValue !== null) {
- this.afterBind = this.listener;
- }
-
- // All major browsers except Firefox resets
- // selectedIndex with value -1 to 0 when the element
- // is appended to a new parent, therefore we have to
- // force a DOM update whenever that happens...
- this.vm.$on('hook:attached', function () {
- nextTick(_this.forceUpdate);
- });
- if (!inDoc(el)) {
- nextTick(this.forceUpdate);
- }
- },
-
- update: function update(value) {
- var el = this.el;
- el.selectedIndex = -1;
- var multi = this.multiple && isArray(value);
- var options = el.options;
- var i = options.length;
- var op, val;
- while (i--) {
- op = options[i];
- val = op.hasOwnProperty('_value') ? op._value : op.value;
- /* eslint-disable eqeqeq */
- op.selected = multi ? indexOf$1(value, val) > -1 : looseEqual(value, val);
- /* eslint-enable eqeqeq */
}
- },
-
- unbind: function unbind() {
- /* istanbul ignore next */
- this.vm.$off('hook:attached', this.forceUpdate);
}
- };
-
- /**
- * Get select value
- *
- * @param {SelectElement} el
- * @param {Boolean} multi
- * @param {Boolean} init
- * @return {Array|*}
- */
-
- function getValue(el, multi, init) {
- var res = multi ? [] : null;
- var op, val, selected;
- for (var i = 0, l = el.options.length; i < l; i++) {
- op = el.options[i];
- selected = init ? op.hasAttribute('selected') : op.selected;
- if (selected) {
- val = op.hasOwnProperty('_value') ? op._value : op.value;
- if (multi) {
- res.push(val);
- } else {
- return val;
- }
- }
+ return res.slice(0, -1)
+ }
+ if (isObject(value)) {
+ for (var key in value) {
+ if (value[key]) { res += key + ' '; }
}
- return res;
+ return res.slice(0, -1)
+ }
+ /* istanbul ignore next */
+ return res
+}
+
+/* */
+
+var namespaceMap = {
+ svg: 'http://www.w3.org/2000/svg',
+ math: 'http://www.w3.org/1998/Math/MathML'
+};
+
+var isHTMLTag = makeMap(
+ 'html,body,base,head,link,meta,style,title,' +
+ 'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' +
+ 'div,dd,dl,dt,figcaption,figure,hr,img,li,main,ol,p,pre,ul,' +
+ 'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' +
+ 's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' +
+ 'embed,object,param,source,canvas,script,noscript,del,ins,' +
+ 'caption,col,colgroup,table,thead,tbody,td,th,tr,' +
+ 'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' +
+ 'output,progress,select,textarea,' +
+ 'details,dialog,menu,menuitem,summary,' +
+ 'content,element,shadow,template'
+);
+
+var isUnaryTag = makeMap(
+ 'area,base,br,col,embed,frame,hr,img,input,isindex,keygen,' +
+ 'link,meta,param,source,track,wbr',
+ true
+);
+
+// Elements that you can, intentionally, leave open
+// (and which close themselves)
+var canBeLeftOpenTag = makeMap(
+ 'colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source',
+ true
+);
+
+// HTML5 tags https://html.spec.whatwg.org/multipage/indices.html#elements-3
+// Phrasing Content https://html.spec.whatwg.org/multipage/dom.html#phrasing-content
+var isNonPhrasingTag = makeMap(
+ 'address,article,aside,base,blockquote,body,caption,col,colgroup,dd,' +
+ 'details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,' +
+ 'h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,' +
+ 'optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,' +
+ 'title,tr,track',
+ true
+);
+
+// this map is intentionally selective, only covering SVG elements that may
+// contain child elements.
+var isSVG = makeMap(
+ 'svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font,' +
+ 'font-face,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,' +
+ 'polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view',
+ true
+);
+
+var isPreTag = function (tag) { return tag === 'pre'; };
+
+var isReservedTag = function (tag) {
+ return isHTMLTag(tag) || isSVG(tag)
+};
+
+function getTagNamespace (tag) {
+ if (isSVG(tag)) {
+ return 'svg'
+ }
+ // basic support for MathML
+ // note it doesn't support other MathML elements being component roots
+ if (tag === 'math') {
+ return 'math'
+ }
+}
+
+var unknownElementCache = Object.create(null);
+function isUnknownElement (tag) {
+ /* istanbul ignore if */
+ if (!inBrowser) {
+ return true
+ }
+ if (isReservedTag(tag)) {
+ return false
+ }
+ tag = tag.toLowerCase();
+ /* istanbul ignore if */
+ if (unknownElementCache[tag] != null) {
+ return unknownElementCache[tag]
+ }
+ var el = document.createElement(tag);
+ if (tag.indexOf('-') > -1) {
+ // http://stackoverflow.com/a/28210364/1070244
+ return (unknownElementCache[tag] = (
+ el.constructor === window.HTMLUnknownElement ||
+ el.constructor === window.HTMLElement
+ ))
+ } else {
+ return (unknownElementCache[tag] = /HTMLUnknownElement/.test(el.toString()))
}
+}
- /**
- * Native Array.indexOf uses strict equal, but in this
- * case we need to match string/numbers with custom equal.
- *
- * @param {Array} arr
- * @param {*} val
- */
+/* */
- function indexOf$1(arr, val) {
- var i = arr.length;
- while (i--) {
- if (looseEqual(arr[i], val)) {
- return i;
+/**
+ * Query an element selector if it's not an element already.
+ */
+function query (el) {
+ if (typeof el === 'string') {
+ var selector = el;
+ el = document.querySelector(el);
+ if (!el) {
+ "development" !== 'production' && warn(
+ 'Cannot find element: ' + selector
+ );
+ return document.createElement('div')
+ }
+ }
+ return el
+}
+
+/* */
+
+function createElement$1 (tagName, vnode) {
+ var elm = document.createElement(tagName);
+ if (tagName !== 'select') {
+ return elm
+ }
+ if (vnode.data && vnode.data.attrs && 'multiple' in vnode.data.attrs) {
+ elm.setAttribute('multiple', 'multiple');
+ }
+ return elm
+}
+
+function createElementNS (namespace, tagName) {
+ return document.createElementNS(namespaceMap[namespace], tagName)
+}
+
+function createTextNode (text) {
+ return document.createTextNode(text)
+}
+
+function createComment (text) {
+ return document.createComment(text)
+}
+
+function insertBefore (parentNode, newNode, referenceNode) {
+ parentNode.insertBefore(newNode, referenceNode);
+}
+
+function removeChild (node, child) {
+ node.removeChild(child);
+}
+
+function appendChild (node, child) {
+ node.appendChild(child);
+}
+
+function parentNode (node) {
+ return node.parentNode
+}
+
+function nextSibling (node) {
+ return node.nextSibling
+}
+
+function tagName (node) {
+ return node.tagName
+}
+
+function setTextContent (node, text) {
+ node.textContent = text;
+}
+
+function childNodes (node) {
+ return node.childNodes
+}
+
+function setAttribute (node, key, val) {
+ node.setAttribute(key, val);
+}
+
+
+var nodeOps = Object.freeze({
+ createElement: createElement$1,
+ createElementNS: createElementNS,
+ createTextNode: createTextNode,
+ createComment: createComment,
+ insertBefore: insertBefore,
+ removeChild: removeChild,
+ appendChild: appendChild,
+ parentNode: parentNode,
+ nextSibling: nextSibling,
+ tagName: tagName,
+ setTextContent: setTextContent,
+ childNodes: childNodes,
+ setAttribute: setAttribute
+});
+
+/* */
+
+var ref = {
+ create: function create (_, vnode) {
+ registerRef(vnode);
+ },
+ update: function update (oldVnode, vnode) {
+ if (oldVnode.data.ref !== vnode.data.ref) {
+ registerRef(oldVnode, true);
+ registerRef(vnode);
+ }
+ },
+ destroy: function destroy (vnode) {
+ registerRef(vnode, true);
+ }
+};
+
+function registerRef (vnode, isRemoval) {
+ var key = vnode.data.ref;
+ if (!key) { return }
+
+ var vm = vnode.context;
+ var ref = vnode.child || vnode.elm;
+ var refs = vm.$refs;
+ if (isRemoval) {
+ if (Array.isArray(refs[key])) {
+ remove$1(refs[key], ref);
+ } else if (refs[key] === ref) {
+ refs[key] = undefined;
+ }
+ } else {
+ if (vnode.data.refInFor) {
+ if (Array.isArray(refs[key])) {
+ refs[key].push(ref);
+ } else {
+ refs[key] = [ref];
}
+ } else {
+ refs[key] = ref;
}
- return -1;
}
+}
- var checkbox = {
+/**
+ * Virtual DOM patching algorithm based on Snabbdom by
+ * Simon Friis Vindum (@paldepind)
+ * Licensed under the MIT License
+ * https://github.com/paldepind/snabbdom/blob/master/LICENSE
+ *
+ * modified by Evan You (@yyx990803)
+ *
- bind: function bind() {
- var self = this;
- var el = this.el;
+/*
+ * Not type-checking this because this file is perf-critical and the cost
+ * of making flow understand it is not worth it.
+ */
- this.getValue = function () {
- return el.hasOwnProperty('_value') ? el._value : self.params.number ? toNumber(el.value) : el.value;
- };
+var emptyNode = new VNode('', {}, []);
- function getBooleanValue() {
- var val = el.checked;
- if (val && el.hasOwnProperty('_trueValue')) {
- return el._trueValue;
- }
- if (!val && el.hasOwnProperty('_falseValue')) {
- return el._falseValue;
- }
- return val;
- }
+var hooks$1 = ['create', 'update', 'remove', 'destroy'];
- this.listener = function () {
- var model = self._watcher.value;
- if (isArray(model)) {
- var val = self.getValue();
- if (el.checked) {
- if (indexOf(model, val) < 0) {
- model.push(val);
- }
- } else {
- model.$remove(val);
- }
- } else {
- self.set(getBooleanValue());
- }
- };
+function isUndef (s) {
+ return s == null
+}
- this.on('change', this.listener);
- if (el.hasAttribute('checked')) {
- this.afterBind = this.listener;
- }
- },
+function isDef (s) {
+ return s != null
+}
- update: function update(value) {
- var el = this.el;
- if (isArray(value)) {
- el.checked = indexOf(value, this.getValue()) > -1;
- } else {
- if (el.hasOwnProperty('_trueValue')) {
- el.checked = looseEqual(value, el._trueValue);
- } else {
- el.checked = !!value;
- }
- }
- }
- };
+function sameVnode (vnode1, vnode2) {
+ return (
+ vnode1.key === vnode2.key &&
+ vnode1.tag === vnode2.tag &&
+ vnode1.isComment === vnode2.isComment &&
+ !vnode1.data === !vnode2.data
+ )
+}
- var handlers = {
- text: text$2,
- radio: radio,
- select: select,
- checkbox: checkbox
- };
+function createKeyToOldIdx (children, beginIdx, endIdx) {
+ var i, key;
+ var map = {};
+ for (i = beginIdx; i <= endIdx; ++i) {
+ key = children[i].key;
+ if (isDef(key)) { map[key] = i; }
+ }
+ return map
+}
- var model = {
-
- priority: MODEL,
- twoWay: true,
- handlers: handlers,
- params: ['lazy', 'number', 'debounce'],
-
- /**
- * Possible elements:
- * <select>
- * <textarea>
- * <input type="*">
- * - text
- * - checkbox
- * - radio
- * - number
- */
-
- bind: function bind() {
- // friendly warning...
- this.checkFilters();
- if (this.hasRead && !this.hasWrite) {
- 'development' !== 'production' && warn('It seems you are using a read-only filter with ' + 'v-model="' + this.descriptor.raw + '". ' + 'You might want to use a two-way filter to ensure correct behavior.', this.vm);
- }
- var el = this.el;
- var tag = el.tagName;
- var handler;
- if (tag === 'INPUT') {
- handler = handlers[el.type] || handlers.text;
- } else if (tag === 'SELECT') {
- handler = handlers.select;
- } else if (tag === 'TEXTAREA') {
- handler = handlers.text;
- } else {
- 'development' !== 'production' && warn('v-model does not support element type: ' + tag, this.vm);
- return;
- }
- el.__v_model = this;
- handler.bind.call(this);
- this.update = handler.update;
- this._unbind = handler.unbind;
- },
+function createPatchFunction (backend) {
+ var i, j;
+ var cbs = {};
- /**
- * Check read/write filter stats.
- */
-
- checkFilters: function checkFilters() {
- var filters = this.filters;
- if (!filters) return;
- var i = filters.length;
- while (i--) {
- var filter = resolveAsset(this.vm.$options, 'filters', filters[i].name);
- if (typeof filter === 'function' || filter.read) {
- this.hasRead = true;
- }
- if (filter.write) {
- this.hasWrite = true;
- }
- }
- },
+ var modules = backend.modules;
+ var nodeOps = backend.nodeOps;
- unbind: function unbind() {
- this.el.__v_model = null;
- this._unbind && this._unbind();
+ for (i = 0; i < hooks$1.length; ++i) {
+ cbs[hooks$1[i]] = [];
+ for (j = 0; j < modules.length; ++j) {
+ if (modules[j][hooks$1[i]] !== undefined) { cbs[hooks$1[i]].push(modules[j][hooks$1[i]]); }
}
- };
+ }
- // keyCode aliases
- var keyCodes = {
- esc: 27,
- tab: 9,
- enter: 13,
- space: 32,
- 'delete': [8, 46],
- up: 38,
- left: 37,
- right: 39,
- down: 40
- };
+ function emptyNodeAt (elm) {
+ return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
+ }
- function keyFilter(handler, keys) {
- var codes = keys.map(function (key) {
- var charCode = key.charCodeAt(0);
- if (charCode > 47 && charCode < 58) {
- return parseInt(key, 10);
+ function createRmCb (childElm, listeners) {
+ function remove$$1 () {
+ if (--remove$$1.listeners === 0) {
+ removeElement(childElm);
}
- if (key.length === 1) {
- charCode = key.toUpperCase().charCodeAt(0);
- if (charCode > 64 && charCode < 91) {
- return charCode;
- }
- }
- return keyCodes[key];
- });
- codes = [].concat.apply([], codes);
- return function keyHandler(e) {
- if (codes.indexOf(e.keyCode) > -1) {
- return handler.call(this, e);
- }
- };
+ }
+ remove$$1.listeners = listeners;
+ return remove$$1
}
- function stopFilter(handler) {
- return function stopHandler(e) {
- e.stopPropagation();
- return handler.call(this, e);
- };
+ function removeElement (el) {
+ var parent = nodeOps.parentNode(el);
+ nodeOps.removeChild(parent, el);
}
- function preventFilter(handler) {
- return function preventHandler(e) {
- e.preventDefault();
- return handler.call(this, e);
- };
+ function createElm (vnode, insertedVnodeQueue, nested) {
+ var i;
+ var data = vnode.data;
+ vnode.isRootInsert = !nested;
+ if (isDef(data)) {
+ if (isDef(i = data.hook) && isDef(i = i.init)) { i(vnode); }
+ // after calling the init hook, if the vnode is a child component
+ // it should've created a child instance and mounted it. the child
+ // component also has set the placeholder vnode's elm.
+ // in that case we can just return the element and be done.
+ if (isDef(i = vnode.child)) {
+ initComponent(vnode, insertedVnodeQueue);
+ return vnode.elm
+ }
+ }
+ var children = vnode.children;
+ var tag = vnode.tag;
+ if (isDef(tag)) {
+ {
+ if (
+ !vnode.ns &&
+ !(config.ignoredElements && config.ignoredElements.indexOf(tag) > -1) &&
+ config.isUnknownElement(tag)
+ ) {
+ warn(
+ 'Unknown custom element: <' + tag + '> - did you ' +
+ 'register the component correctly? For recursive components, ' +
+ 'make sure to provide the "name" option.',
+ vnode.context
+ );
+ }
+ }
+ vnode.elm = vnode.ns
+ ? nodeOps.createElementNS(vnode.ns, tag)
+ : nodeOps.createElement(tag, vnode);
+ setScope(vnode);
+ createChildren(vnode, children, insertedVnodeQueue);
+ if (isDef(data)) {
+ invokeCreateHooks(vnode, insertedVnodeQueue);
+ }
+ } else if (vnode.isComment) {
+ vnode.elm = nodeOps.createComment(vnode.text);
+ } else {
+ vnode.elm = nodeOps.createTextNode(vnode.text);
+ }
+ return vnode.elm
}
- function selfFilter(handler) {
- return function selfHandler(e) {
- if (e.target === e.currentTarget) {
- return handler.call(this, e);
+ function createChildren (vnode, children, insertedVnodeQueue) {
+ if (Array.isArray(children)) {
+ for (var i = 0; i < children.length; ++i) {
+ nodeOps.appendChild(vnode.elm, createElm(children[i], insertedVnodeQueue, true));
}
- };
+ } else if (isPrimitive(vnode.text)) {
+ nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(vnode.text));
+ }
}
- var on$1 = {
-
- priority: ON,
- acceptStatement: true,
- keyCodes: keyCodes,
-
- bind: function bind() {
- // deal with iframes
- if (this.el.tagName === 'IFRAME' && this.arg !== 'load') {
- var self = this;
- this.iframeBind = function () {
- on(self.el.contentWindow, self.arg, self.handler, self.modifiers.capture);
- };
- this.on('load', this.iframeBind);
- }
- },
-
- update: function update(handler) {
- // stub a noop for v-on with no value,
- // e.g. @mousedown.prevent
- if (!this.descriptor.raw) {
- handler = function () {};
- }
-
- if (typeof handler !== 'function') {
- 'development' !== 'production' && warn('v-on:' + this.arg + '="' + this.expression + '" expects a function value, ' + 'got ' + handler, this.vm);
- return;
- }
-
- // apply modifiers
- if (this.modifiers.stop) {
- handler = stopFilter(handler);
- }
- if (this.modifiers.prevent) {
- handler = preventFilter(handler);
- }
- if (this.modifiers.self) {
- handler = selfFilter(handler);
- }
- // key filter
- var keys = Object.keys(this.modifiers).filter(function (key) {
- return key !== 'stop' && key !== 'prevent' && key !== 'self' && key !== 'capture';
- });
- if (keys.length) {
- handler = keyFilter(handler, keys);
- }
-
- this.reset();
- this.handler = handler;
-
- if (this.iframeBind) {
- this.iframeBind();
- } else {
- on(this.el, this.arg, this.handler, this.modifiers.capture);
- }
- },
-
- reset: function reset() {
- var el = this.iframeBind ? this.el.contentWindow : this.el;
- if (this.handler) {
- off(el, this.arg, this.handler);
- }
- },
-
- unbind: function unbind() {
- this.reset();
+ function isPatchable (vnode) {
+ while (vnode.child) {
+ vnode = vnode.child._vnode;
}
- };
-
- var prefixes = ['-webkit-', '-moz-', '-ms-'];
- var camelPrefixes = ['Webkit', 'Moz', 'ms'];
- var importantRE = /!important;?$/;
- var propCache = Object.create(null);
-
- var testEl = null;
-
- var style = {
-
- deep: true,
-
- update: function update(value) {
- if (typeof value === 'string') {
- this.el.style.cssText = value;
- } else if (isArray(value)) {
- this.handleObject(value.reduce(extend, {}));
- } else {
- this.handleObject(value || {});
- }
- },
-
- handleObject: function handleObject(value) {
- // cache object styles so that only changed props
- // are actually updated.
- var cache = this.cache || (this.cache = {});
- var name, val;
- for (name in cache) {
- if (!(name in value)) {
- this.handleSingle(name, null);
- delete cache[name];
- }
- }
- for (name in value) {
- val = value[name];
- if (val !== cache[name]) {
- cache[name] = val;
- this.handleSingle(name, val);
- }
- }
- },
+ return isDef(vnode.tag)
+ }
- handleSingle: function handleSingle(prop, value) {
- prop = normalize(prop);
- if (!prop) return; // unsupported prop
- // cast possible numbers/booleans into strings
- if (value != null) value += '';
- if (value) {
- var isImportant = importantRE.test(value) ? 'important' : '';
- if (isImportant) {
- /* istanbul ignore if */
- if ('development' !== 'production') {
- warn('It\'s probably a bad idea to use !important with inline rules. ' + 'This feature will be deprecated in a future version of Vue.');
- }
- value = value.replace(importantRE, '').trim();
- this.el.style.setProperty(prop.kebab, value, isImportant);
- } else {
- this.el.style[prop.camel] = value;
- }
- } else {
- this.el.style[prop.camel] = '';
- }
+ function invokeCreateHooks (vnode, insertedVnodeQueue) {
+ for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
+ cbs.create[i$1](emptyNode, vnode);
}
-
- };
-
- /**
- * Normalize a CSS property name.
- * - cache result
- * - auto prefix
- * - camelCase -> dash-case
- *
- * @param {String} prop
- * @return {String}
- */
-
- function normalize(prop) {
- if (propCache[prop]) {
- return propCache[prop];
+ i = vnode.data.hook; // Reuse variable
+ if (isDef(i)) {
+ if (i.create) { i.create(emptyNode, vnode); }
+ if (i.insert) { insertedVnodeQueue.push(vnode); }
}
- var res = prefix(prop);
- propCache[prop] = propCache[res] = res;
- return res;
}
- /**
- * Auto detect the appropriate prefix for a CSS property.
- * https://gist.github.com/paulirish/523692
- *
- * @param {String} prop
- * @return {String}
- */
-
- function prefix(prop) {
- prop = hyphenate(prop);
- var camel = camelize(prop);
- var upper = camel.charAt(0).toUpperCase() + camel.slice(1);
- if (!testEl) {
- testEl = document.createElement('div');
- }
- var i = prefixes.length;
- var prefixed;
- if (camel !== 'filter' && camel in testEl.style) {
- return {
- kebab: prop,
- camel: camel
- };
+ function initComponent (vnode, insertedVnodeQueue) {
+ if (vnode.data.pendingInsert) {
+ insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
}
- while (i--) {
- prefixed = camelPrefixes[i] + upper;
- if (prefixed in testEl.style) {
- return {
- kebab: prefixes[i] + prop,
- camel: prefixed
- };
- }
- }
- }
-
- // xlink
- var xlinkNS = 'http://www.w3.org/1999/xlink';
- var xlinkRE = /^xlink:/;
-
- // check for attributes that prohibit interpolations
- var disallowedInterpAttrRE = /^v-|^:|^@|^(?:is|transition|transition-mode|debounce|track-by|stagger|enter-stagger|leave-stagger)$/;
- // these attributes should also set their corresponding properties
- // because they only affect the initial state of the element
- var attrWithPropsRE = /^(?:value|checked|selected|muted)$/;
- // these attributes expect enumrated values of "true" or "false"
- // but are not boolean attributes
- var enumeratedAttrRE = /^(?:draggable|contenteditable|spellcheck)$/;
-
- // these attributes should set a hidden property for
- // binding v-model to object values
- var modelProps = {
- value: '_value',
- 'true-value': '_trueValue',
- 'false-value': '_falseValue'
- };
+ vnode.elm = vnode.child.$el;
+ if (isPatchable(vnode)) {
+ invokeCreateHooks(vnode, insertedVnodeQueue);
+ setScope(vnode);
+ } else {
+ // empty component root.
+ // skip all element-related modules except for ref (#3455)
+ registerRef(vnode);
+ // make sure to invoke the insert hook
+ insertedVnodeQueue.push(vnode);
+ }
+ }
- var bind$1 = {
+ // set scope id attribute for scoped CSS.
+ // this is implemented as a special case to avoid the overhead
+ // of going through the normal attribute patching process.
+ function setScope (vnode) {
+ var i;
+ if (isDef(i = vnode.context) && isDef(i = i.$options._scopeId)) {
+ nodeOps.setAttribute(vnode.elm, i, '');
+ }
+ if (isDef(i = activeInstance) &&
+ i !== vnode.context &&
+ isDef(i = i.$options._scopeId)) {
+ nodeOps.setAttribute(vnode.elm, i, '');
+ }
+ }
- priority: BIND,
+ function addVnodes (parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
+ for (; startIdx <= endIdx; ++startIdx) {
+ nodeOps.insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue), before);
+ }
+ }
- bind: function bind() {
- var attr = this.arg;
- var tag = this.el.tagName;
- // should be deep watch on object mode
- if (!attr) {
- this.deep = true;
+ function invokeDestroyHook (vnode) {
+ var i, j;
+ var data = vnode.data;
+ if (isDef(data)) {
+ if (isDef(i = data.hook) && isDef(i = i.destroy)) { i(vnode); }
+ for (i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](vnode); }
+ }
+ if (isDef(i = vnode.children)) {
+ for (j = 0; j < vnode.children.length; ++j) {
+ invokeDestroyHook(vnode.children[j]);
}
- // handle interpolation bindings
- var descriptor = this.descriptor;
- var tokens = descriptor.interp;
- if (tokens) {
- // handle interpolations with one-time tokens
- if (descriptor.hasOneTime) {
- this.expression = tokensToExp(tokens, this._scope || this.vm);
- }
-
- // only allow binding on native attributes
- if (disallowedInterpAttrRE.test(attr) || attr === 'name' && (tag === 'PARTIAL' || tag === 'SLOT')) {
- 'development' !== 'production' && warn(attr + '="' + descriptor.raw + '": ' + 'attribute interpolation is not allowed in Vue.js ' + 'directives and special attributes.', this.vm);
- this.el.removeAttribute(attr);
- this.invalid = true;
- }
-
- /* istanbul ignore if */
- if ('development' !== 'production') {
- var raw = attr + '="' + descriptor.raw + '": ';
- // warn src
- if (attr === 'src') {
- warn(raw + 'interpolation in "src" attribute will cause ' + 'a 404 request. Use v-bind:src instead.', this.vm);
- }
+ }
+ }
- // warn style
- if (attr === 'style') {
- warn(raw + 'interpolation in "style" attribute will cause ' + 'the attribute to be discarded in Internet Explorer. ' + 'Use v-bind:style instead.', this.vm);
- }
+ function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
+ for (; startIdx <= endIdx; ++startIdx) {
+ var ch = vnodes[startIdx];
+ if (isDef(ch)) {
+ if (isDef(ch.tag)) {
+ removeAndInvokeRemoveHook(ch);
+ invokeDestroyHook(ch);
+ } else { // Text node
+ nodeOps.removeChild(parentElm, ch.elm);
}
}
- },
+ }
+ }
- update: function update(value) {
- if (this.invalid) {
- return;
- }
- var attr = this.arg;
- if (this.arg) {
- this.handleSingle(attr, value);
+ function removeAndInvokeRemoveHook (vnode, rm) {
+ if (rm || isDef(vnode.data)) {
+ var listeners = cbs.remove.length + 1;
+ if (!rm) {
+ // directly removing
+ rm = createRmCb(vnode.elm, listeners);
} else {
- this.handleObject(value || {});
+ // we have a recursively passed down rm callback
+ // increase the listeners count
+ rm.listeners += listeners;
}
- },
-
- // share object handler with v-bind:class
- handleObject: style.handleObject,
-
- handleSingle: function handleSingle(attr, value) {
- var el = this.el;
- var interp = this.descriptor.interp;
- if (this.modifiers.camel) {
- attr = camelize(attr);
+ // recursively invoke hooks on child component root node
+ if (isDef(i = vnode.child) && isDef(i = i._vnode) && isDef(i.data)) {
+ removeAndInvokeRemoveHook(i, rm);
}
- if (!interp && attrWithPropsRE.test(attr) && attr in el) {
- var attrValue = attr === 'value' ? value == null // IE9 will set input.value to "null" for null...
- ? '' : value : value;
-
- if (el[attr] !== attrValue) {
- el[attr] = attrValue;
- }
+ for (i = 0; i < cbs.remove.length; ++i) {
+ cbs.remove[i](vnode, rm);
}
- // set model props
- var modelProp = modelProps[attr];
- if (!interp && modelProp) {
- el[modelProp] = value;
- // update v-model if present
- var model = el.__v_model;
- if (model) {
- model.listener();
- }
+ if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
+ i(vnode, rm);
+ } else {
+ rm();
}
- // do not set value attribute for textarea
- if (attr === 'value' && el.tagName === 'TEXTAREA') {
- el.removeAttribute(attr);
- return;
- }
- // update attribute
- if (enumeratedAttrRE.test(attr)) {
- el.setAttribute(attr, value ? 'true' : 'false');
- } else if (value != null && value !== false) {
- if (attr === 'class') {
- // handle edge case #1960:
- // class interpolation should not overwrite Vue transition class
- if (el.__v_trans) {
- value += ' ' + el.__v_trans.id + '-transition';
- }
- setClass(el, value);
- } else if (xlinkRE.test(attr)) {
- el.setAttributeNS(xlinkNS, attr, value === true ? '' : value);
+ } else {
+ removeElement(vnode.elm);
+ }
+ }
+
+ function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
+ var oldStartIdx = 0;
+ var newStartIdx = 0;
+ var oldEndIdx = oldCh.length - 1;
+ var oldStartVnode = oldCh[0];
+ var oldEndVnode = oldCh[oldEndIdx];
+ var newEndIdx = newCh.length - 1;
+ var newStartVnode = newCh[0];
+ var newEndVnode = newCh[newEndIdx];
+ var oldKeyToIdx, idxInOld, elmToMove, before;
+
+ // removeOnly is a special flag used only by <transition-group>
+ // to ensure removed elements stay in correct relative positions
+ // during leaving transitions
+ var canMove = !removeOnly;
+
+ while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
+ if (isUndef(oldStartVnode)) {
+ oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
+ } else if (isUndef(oldEndVnode)) {
+ oldEndVnode = oldCh[--oldEndIdx];
+ } else if (sameVnode(oldStartVnode, newStartVnode)) {
+ patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
+ oldStartVnode = oldCh[++oldStartIdx];
+ newStartVnode = newCh[++newStartIdx];
+ } else if (sameVnode(oldEndVnode, newEndVnode)) {
+ patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
+ oldEndVnode = oldCh[--oldEndIdx];
+ newEndVnode = newCh[--newEndIdx];
+ } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
+ patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
+ canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
+ oldStartVnode = oldCh[++oldStartIdx];
+ newEndVnode = newCh[--newEndIdx];
+ } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
+ patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
+ canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
+ oldEndVnode = oldCh[--oldEndIdx];
+ newStartVnode = newCh[++newStartIdx];
+ } else {
+ if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); }
+ idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null;
+ if (isUndef(idxInOld)) { // New element
+ nodeOps.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
+ newStartVnode = newCh[++newStartIdx];
} else {
- el.setAttribute(attr, value === true ? '' : value);
+ elmToMove = oldCh[idxInOld];
+ /* istanbul ignore if */
+ if ("development" !== 'production' && !elmToMove) {
+ warn(
+ 'It seems there are duplicate keys that is causing an update error. ' +
+ 'Make sure each v-for item has a unique key.'
+ );
+ }
+ if (elmToMove.tag !== newStartVnode.tag) {
+ // same key but different element. treat as new element
+ nodeOps.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
+ newStartVnode = newCh[++newStartIdx];
+ } else {
+ patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
+ oldCh[idxInOld] = undefined;
+ canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm);
+ newStartVnode = newCh[++newStartIdx];
+ }
}
- } else {
- el.removeAttribute(attr);
}
}
- };
-
- var el = {
-
- priority: EL,
-
- bind: function bind() {
- /* istanbul ignore if */
- if (!this.arg) {
- return;
- }
- var id = this.id = camelize(this.arg);
- var refs = (this._scope || this.vm).$els;
- if (hasOwn(refs, id)) {
- refs[id] = this.el;
- } else {
- defineReactive(refs, id, this.el);
- }
- },
-
- unbind: function unbind() {
- var refs = (this._scope || this.vm).$els;
- if (refs[this.id] === this.el) {
- refs[this.id] = null;
- }
+ if (oldStartIdx > oldEndIdx) {
+ before = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
+ addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
+ } else if (newStartIdx > newEndIdx) {
+ removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
- };
+ }
- var ref = {
- bind: function bind() {
- 'development' !== 'production' && warn('v-ref:' + this.arg + ' must be used on a child ' + 'component. Found on <' + this.el.tagName.toLowerCase() + '>.', this.vm);
+ function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
+ if (oldVnode === vnode) {
+ return
}
- };
-
- var cloak = {
- bind: function bind() {
- var el = this.el;
- this.vm.$once('pre-hook:compiled', function () {
- el.removeAttribute('v-cloak');
- });
+ // reuse element for static trees.
+ // note we only do this if the vnode is cloned -
+ // if the new node is not cloned it means the render functions have been
+ // reset by the hot-reload-api and we need to do a proper re-render.
+ if (vnode.isStatic &&
+ oldVnode.isStatic &&
+ vnode.key === oldVnode.key &&
+ vnode.isCloned) {
+ vnode.elm = oldVnode.elm;
+ return
}
- };
-
- // must export plain object
- var directives = {
- text: text$1,
- html: html,
- 'for': vFor,
- 'if': vIf,
- show: show,
- model: model,
- on: on$1,
- bind: bind$1,
- el: el,
- ref: ref,
- cloak: cloak
- };
-
- var vClass = {
-
- deep: true,
-
- update: function update(value) {
- if (!value) {
- this.cleanup();
- } else if (typeof value === 'string') {
- this.setClass(value.trim().split(/\s+/));
- } else {
- this.setClass(normalize$1(value));
+ var i;
+ var data = vnode.data;
+ var hasData = isDef(data);
+ if (hasData && isDef(i = data.hook) && isDef(i = i.prepatch)) {
+ i(oldVnode, vnode);
+ }
+ var elm = vnode.elm = oldVnode.elm;
+ var oldCh = oldVnode.children;
+ var ch = vnode.children;
+ if (hasData && isPatchable(vnode)) {
+ for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); }
+ if (isDef(i = data.hook) && isDef(i = i.update)) { i(oldVnode, vnode); }
+ }
+ if (isUndef(vnode.text)) {
+ if (isDef(oldCh) && isDef(ch)) {
+ if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
+ } else if (isDef(ch)) {
+ if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); }
+ addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
+ } else if (isDef(oldCh)) {
+ removeVnodes(elm, oldCh, 0, oldCh.length - 1);
+ } else if (isDef(oldVnode.text)) {
+ nodeOps.setTextContent(elm, '');
+ }
+ } else if (oldVnode.text !== vnode.text) {
+ nodeOps.setTextContent(elm, vnode.text);
+ }
+ if (hasData) {
+ if (isDef(i = data.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode); }
+ }
+ }
+
+ function invokeInsertHook (vnode, queue, initial) {
+ // delay insert hooks for component root nodes, invoke them after the
+ // element is really inserted
+ if (initial && vnode.parent) {
+ vnode.parent.data.pendingInsert = queue;
+ } else {
+ for (var i = 0; i < queue.length; ++i) {
+ queue[i].data.hook.insert(queue[i]);
}
- },
+ }
+ }
- setClass: function setClass(value) {
- this.cleanup(value);
- for (var i = 0, l = value.length; i < l; i++) {
- var val = value[i];
- if (val) {
- apply(this.el, val, addClass);
- }
+ var bailed = false;
+ function hydrate (elm, vnode, insertedVnodeQueue) {
+ {
+ if (!assertNodeMatch(elm, vnode)) {
+ return false
}
- this.prevKeys = value;
- },
-
- cleanup: function cleanup(value) {
- var prevKeys = this.prevKeys;
- if (!prevKeys) return;
- var i = prevKeys.length;
- while (i--) {
- var key = prevKeys[i];
- if (!value || value.indexOf(key) < 0) {
- apply(this.el, key, removeClass);
- }
+ }
+ vnode.elm = elm;
+ var tag = vnode.tag;
+ var data = vnode.data;
+ var children = vnode.children;
+ if (isDef(data)) {
+ if (isDef(i = data.hook) && isDef(i = i.init)) { i(vnode, true /* hydrating */); }
+ if (isDef(i = vnode.child)) {
+ // child component. it should have hydrated its own tree.
+ initComponent(vnode, insertedVnodeQueue);
+ return true
}
}
- };
-
- /**
- * Normalize objects and arrays (potentially containing objects)
- * into array of strings.
- *
- * @param {Object|Array<String|Object>} value
- * @return {Array<String>}
- */
-
- function normalize$1(value) {
- var res = [];
- if (isArray(value)) {
- for (var i = 0, l = value.length; i < l; i++) {
- var _key = value[i];
- if (_key) {
- if (typeof _key === 'string') {
- res.push(_key);
+ if (isDef(tag)) {
+ if (isDef(children)) {
+ var childNodes = nodeOps.childNodes(elm);
+ // empty element, allow client to pick up and populate children
+ if (!childNodes.length) {
+ createChildren(vnode, children, insertedVnodeQueue);
+ } else {
+ var childrenMatch = true;
+ if (childNodes.length !== children.length) {
+ childrenMatch = false;
} else {
- for (var k in _key) {
- if (_key[k]) res.push(k);
+ for (var i$1 = 0; i$1 < children.length; i$1++) {
+ if (!hydrate(childNodes[i$1], children[i$1], insertedVnodeQueue)) {
+ childrenMatch = false;
+ break
+ }
}
}
+ if (!childrenMatch) {
+ if ("development" !== 'production' &&
+ typeof console !== 'undefined' &&
+ !bailed) {
+ bailed = true;
+ console.warn('Parent: ', elm);
+ console.warn('Mismatching childNodes vs. VNodes: ', childNodes, children);
+ }
+ return false
+ }
}
}
- } else if (isObject(value)) {
- for (var key in value) {
- if (value[key]) res.push(key);
+ if (isDef(data)) {
+ invokeCreateHooks(vnode, insertedVnodeQueue);
}
}
- return res;
+ return true
}
- /**
- * Add or remove a class/classes on an element
- *
- * @param {Element} el
- * @param {String} key The class name. This may or may not
- * contain a space character, in such a
- * case we'll deal with multiple class
- * names at once.
- * @param {Function} fn
- */
-
- function apply(el, key, fn) {
- key = key.trim();
- if (key.indexOf(' ') === -1) {
- fn(el, key);
- return;
- }
- // The key contains one or more space characters.
- // Since a class name doesn't accept such characters, we
- // treat it as multiple classes.
- var keys = key.split(/\s+/);
- for (var i = 0, l = keys.length; i < l; i++) {
- fn(el, keys[i]);
+ function assertNodeMatch (node, vnode) {
+ if (vnode.tag) {
+ return (
+ vnode.tag.indexOf('vue-component') === 0 ||
+ vnode.tag === nodeOps.tagName(node).toLowerCase()
+ )
+ } else {
+ return _toString(vnode.text) === node.data
}
}
- var component = {
-
- priority: COMPONENT,
-
- params: ['keep-alive', 'transition-mode', 'inline-template'],
-
- /**
- * Setup. Two possible usages:
- *
- * - static:
- * <comp> or <div v-component="comp">
- *
- * - dynamic:
- * <component :is="view">
- */
-
- bind: function bind() {
- if (!this.el.__vue__) {
- // keep-alive cache
- this.keepAlive = this.params.keepAlive;
- if (this.keepAlive) {
- this.cache = {};
- }
- // check inline-template
- if (this.params.inlineTemplate) {
- // extract inline template as a DocumentFragment
- this.inlineTemplate = extractContent(this.el, true);
- }
- // component resolution related state
- this.pendingComponentCb = this.Component = null;
- // transition related state
- this.pendingRemovals = 0;
- this.pendingRemovalCb = null;
- // create a ref anchor
- this.anchor = createAnchor('v-component');
- replace(this.el, this.anchor);
- // remove is attribute.
- // this is removed during compilation, but because compilation is
- // cached, when the component is used elsewhere this attribute
- // will remain at link time.
- this.el.removeAttribute('is');
- this.el.removeAttribute(':is');
- // remove ref, same as above
- if (this.descriptor.ref) {
- this.el.removeAttribute('v-ref:' + hyphenate(this.descriptor.ref));
- }
- // if static, build right now.
- if (this.literal) {
- this.setComponent(this.expression);
- }
- } else {
- 'development' !== 'production' && warn('cannot mount component "' + this.expression + '" ' + 'on already mounted element: ' + this.el);
- }
- },
-
- /**
- * Public update, called by the watcher in the dynamic
- * literal scenario, e.g. <component :is="view">
- */
+ return function patch (oldVnode, vnode, hydrating, removeOnly) {
+ if (!vnode) {
+ if (oldVnode) { invokeDestroyHook(oldVnode); }
+ return
+ }
- update: function update(value) {
- if (!this.literal) {
- this.setComponent(value);
- }
- },
+ var elm, parent;
+ var isInitialPatch = false;
+ var insertedVnodeQueue = [];
- /**
- * Switch dynamic components. May resolve the component
- * asynchronously, and perform transition based on
- * specified transition mode. Accepts a few additional
- * arguments specifically for vue-router.
- *
- * The callback is called when the full transition is
- * finished.
- *
- * @param {String} value
- * @param {Function} [cb]
- */
-
- setComponent: function setComponent(value, cb) {
- this.invalidatePending();
- if (!value) {
- // just remove current
- this.unbuild(true);
- this.remove(this.childVM, cb);
- this.childVM = null;
+ if (!oldVnode) {
+ // empty mount, create new root element
+ isInitialPatch = true;
+ createElm(vnode, insertedVnodeQueue);
+ } else {
+ var isRealElement = isDef(oldVnode.nodeType);
+ if (!isRealElement && sameVnode(oldVnode, vnode)) {
+ patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly);
} else {
- var self = this;
- this.resolveComponent(value, function () {
- self.mountComponent(cb);
- });
- }
- },
-
- /**
- * Resolve the component constructor to use when creating
- * the child vm.
- *
- * @param {String|Function} value
- * @param {Function} cb
- */
-
- resolveComponent: function resolveComponent(value, cb) {
- var self = this;
- this.pendingComponentCb = cancellable(function (Component) {
- self.ComponentName = Component.options.name || (typeof value === 'string' ? value : null);
- self.Component = Component;
- cb();
- });
- this.vm._resolveComponent(value, this.pendingComponentCb);
- },
-
- /**
- * Create a new instance using the current constructor and
- * replace the existing instance. This method doesn't care
- * whether the new component and the old one are actually
- * the same.
- *
- * @param {Function} [cb]
- */
-
- mountComponent: function mountComponent(cb) {
- // actual mount
- this.unbuild(true);
- var self = this;
- var activateHooks = this.Component.options.activate;
- var cached = this.getCached();
- var newComponent = this.build();
- if (activateHooks && !cached) {
- this.waitingFor = newComponent;
- callActivateHooks(activateHooks, newComponent, function () {
- if (self.waitingFor !== newComponent) {
- return;
+ if (isRealElement) {
+ // mounting to a real element
+ // check if this is server-rendered content and if we can perform
+ // a successful hydration.
+ if (oldVnode.nodeType === 1 && oldVnode.hasAttribute('server-rendered')) {
+ oldVnode.removeAttribute('server-rendered');
+ hydrating = true;
}
- self.waitingFor = null;
- self.transition(newComponent, cb);
- });
- } else {
- // update ref for kept-alive component
- if (cached) {
- newComponent._updateRef();
- }
- this.transition(newComponent, cb);
- }
- },
-
- /**
- * When the component changes or unbinds before an async
- * constructor is resolved, we need to invalidate its
- * pending callback.
- */
-
- invalidatePending: function invalidatePending() {
- if (this.pendingComponentCb) {
- this.pendingComponentCb.cancel();
- this.pendingComponentCb = null;
- }
- },
-
- /**
- * Instantiate/insert a new child vm.
- * If keep alive and has cached instance, insert that
- * instance; otherwise build a new one and cache it.
- *
- * @param {Object} [extraOptions]
- * @return {Vue} - the created instance
- */
-
- build: function build(extraOptions) {
- var cached = this.getCached();
- if (cached) {
- return cached;
- }
- if (this.Component) {
- // default options
- var options = {
- name: this.ComponentName,
- el: cloneNode(this.el),
- template: this.inlineTemplate,
- // make sure to add the child with correct parent
- // if this is a transcluded component, its parent
- // should be the transclusion host.
- parent: this._host || this.vm,
- // if no inline-template, then the compiled
- // linker can be cached for better performance.
- _linkerCachable: !this.inlineTemplate,
- _ref: this.descriptor.ref,
- _asComponent: true,
- _isRouterView: this._isRouterView,
- // if this is a transcluded component, context
- // will be the common parent vm of this instance
- // and its host.
- _context: this.vm,
- // if this is inside an inline v-for, the scope
- // will be the intermediate scope created for this
- // repeat fragment. this is used for linking props
- // and container directives.
- _scope: this._scope,
- // pass in the owner fragment of this component.
- // this is necessary so that the fragment can keep
- // track of its contained components in order to
- // call attach/detach hooks for them.
- _frag: this._frag
- };
- // extra options
- // in 1.0.0 this is used by vue-router only
- /* istanbul ignore if */
- if (extraOptions) {
- extend(options, extraOptions);
- }
- var child = new this.Component(options);
- if (this.keepAlive) {
- this.cache[this.Component.cid] = child;
- }
- /* istanbul ignore if */
- if ('development' !== 'production' && this.el.hasAttribute('transition') && child._isFragment) {
- warn('Transitions will not work on a fragment instance. ' + 'Template: ' + child.$options.template, child);
- }
- return child;
- }
- },
-
- /**
- * Try to get a cached instance of the current component.
- *
- * @return {Vue|undefined}
- */
-
- getCached: function getCached() {
- return this.keepAlive && this.cache[this.Component.cid];
- },
-
- /**
- * Teardown the current child, but defers cleanup so
- * that we can separate the destroy and removal steps.
- *
- * @param {Boolean} defer
- */
-
- unbuild: function unbuild(defer) {
- if (this.waitingFor) {
- if (!this.keepAlive) {
- this.waitingFor.$destroy();
- }
- this.waitingFor = null;
- }
- var child = this.childVM;
- if (!child || this.keepAlive) {
- if (child) {
- // remove ref
- child._inactive = true;
- child._updateRef(true);
- }
- return;
- }
- // the sole purpose of `deferCleanup` is so that we can
- // "deactivate" the vm right now and perform DOM removal
- // later.
- child.$destroy(false, defer);
- },
-
- /**
- * Remove current destroyed child and manually do
- * the cleanup after removal.
- *
- * @param {Function} cb
- */
-
- remove: function remove(child, cb) {
- var keepAlive = this.keepAlive;
- if (child) {
- // we may have a component switch when a previous
- // component is still being transitioned out.
- // we want to trigger only one lastest insertion cb
- // when the existing transition finishes. (#1119)
- this.pendingRemovals++;
- this.pendingRemovalCb = cb;
- var self = this;
- child.$remove(function () {
- self.pendingRemovals--;
- if (!keepAlive) child._cleanup();
- if (!self.pendingRemovals && self.pendingRemovalCb) {
- self.pendingRemovalCb();
- self.pendingRemovalCb = null;
+ if (hydrating) {
+ if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
+ invokeInsertHook(vnode, insertedVnodeQueue, true);
+ return oldVnode
+ } else {
+ warn(
+ 'The client-side rendered virtual DOM tree is not matching ' +
+ 'server-rendered content. This is likely caused by incorrect ' +
+ 'HTML markup, for example nesting block-level elements inside ' +
+ '<p>, or missing <tbody>. Bailing hydration and performing ' +
+ 'full client-side render.'
+ );
+ }
}
- });
- } else if (cb) {
- cb();
- }
- },
-
- /**
- * Actually swap the components, depending on the
- * transition mode. Defaults to simultaneous.
- *
- * @param {Vue} target
- * @param {Function} [cb]
- */
-
- transition: function transition(target, cb) {
- var self = this;
- var current = this.childVM;
- // for devtool inspection
- if (current) current._inactive = true;
- target._inactive = false;
- this.childVM = target;
- switch (self.params.transitionMode) {
- case 'in-out':
- target.$before(self.anchor, function () {
- self.remove(current, cb);
- });
- break;
- case 'out-in':
- self.remove(current, function () {
- target.$before(self.anchor, cb);
- });
- break;
- default:
- self.remove(current);
- target.$before(self.anchor, cb);
- }
- },
+ // either not server-rendered, or hydration failed.
+ // create an empty node and replace it
+ oldVnode = emptyNodeAt(oldVnode);
+ }
+ elm = oldVnode.elm;
+ parent = nodeOps.parentNode(elm);
+
+ createElm(vnode, insertedVnodeQueue);
+
+ // component root element replaced.
+ // update parent placeholder node element.
+ if (vnode.parent) {
+ vnode.parent.elm = vnode.elm;
+ if (isPatchable(vnode)) {
+ for (var i = 0; i < cbs.create.length; ++i) {
+ cbs.create[i](emptyNode, vnode.parent);
+ }
+ }
+ }
- /**
- * Unbind.
- */
-
- unbind: function unbind() {
- this.invalidatePending();
- // Do not defer cleanup when unbinding
- this.unbuild();
- // destroy all keep-alive cached instances
- if (this.cache) {
- for (var key in this.cache) {
- this.cache[key].$destroy();
+ if (parent !== null) {
+ nodeOps.insertBefore(parent, vnode.elm, nodeOps.nextSibling(elm));
+ removeVnodes(parent, [oldVnode], 0, 0);
+ } else if (isDef(oldVnode.tag)) {
+ invokeDestroyHook(oldVnode);
}
- this.cache = null;
}
}
- };
-
- /**
- * Call activate hooks in order (asynchronous)
- *
- * @param {Array} hooks
- * @param {Vue} vm
- * @param {Function} cb
- */
- function callActivateHooks(hooks, vm, cb) {
- var total = hooks.length;
- var called = 0;
- hooks[0].call(vm, next);
- function next() {
- if (++called >= total) {
- cb();
- } else {
- hooks[called].call(vm, next);
- }
- }
+ invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
+ return vnode.elm
}
+}
- var propBindingModes = config._propBindingModes;
- var empty = {};
-
- // regexes
- var identRE$1 = /^[$_a-zA-Z]+[\w$]*$/;
- var settablePathRE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\[[^\[\]]+\])*$/;
-
- /**
- * Compile props on a root element and return
- * a props link function.
- *
- * @param {Element|DocumentFragment} el
- * @param {Array} propOptions
- * @param {Vue} vm
- * @return {Function} propsLinkFn
- */
-
- function compileProps(el, propOptions, vm) {
- var props = [];
- var names = Object.keys(propOptions);
- var i = names.length;
- var options, name, attr, value, path, parsed, prop;
- while (i--) {
- name = names[i];
- options = propOptions[name] || empty;
+/* */
- if ('development' !== 'production' && name === '$data') {
- warn('Do not use $data as prop.', vm);
- continue;
- }
+var directives = {
+ create: updateDirectives,
+ update: updateDirectives,
+ destroy: function unbindDirectives (vnode) {
+ updateDirectives(vnode, emptyNode);
+ }
+};
- // props could contain dashes, which will be
- // interpreted as minus calculations by the parser
- // so we need to camelize the path here
- path = camelize(name);
- if (!identRE$1.test(path)) {
- 'development' !== 'production' && warn('Invalid prop key: "' + name + '". Prop keys ' + 'must be valid identifiers.', vm);
- continue;
- }
+function updateDirectives (
+ oldVnode,
+ vnode
+) {
+ if (!oldVnode.data.directives && !vnode.data.directives) {
+ return
+ }
+ var isCreate = oldVnode === emptyNode;
+ var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context);
+ var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context);
- prop = {
- name: name,
- path: path,
- options: options,
- mode: propBindingModes.ONE_WAY,
- raw: null
- };
+ var dirsWithInsert = [];
+ var dirsWithPostpatch = [];
- attr = hyphenate(name);
- // first check dynamic version
- if ((value = getBindAttr(el, attr)) === null) {
- if ((value = getBindAttr(el, attr + '.sync')) !== null) {
- prop.mode = propBindingModes.TWO_WAY;
- } else if ((value = getBindAttr(el, attr + '.once')) !== null) {
- prop.mode = propBindingModes.ONE_TIME;
- }
+ var key, oldDir, dir;
+ for (key in newDirs) {
+ oldDir = oldDirs[key];
+ dir = newDirs[key];
+ if (!oldDir) {
+ // new directive, bind
+ callHook$1(dir, 'bind', vnode, oldVnode);
+ if (dir.def && dir.def.inserted) {
+ dirsWithInsert.push(dir);
}
- if (value !== null) {
- // has dynamic binding!
- prop.raw = value;
- parsed = parseDirective(value);
- value = parsed.expression;
- prop.filters = parsed.filters;
- // check binding type
- if (isLiteral(value) && !parsed.filters) {
- // for expressions containing literal numbers and
- // booleans, there's no need to setup a prop binding,
- // so we can optimize them as a one-time set.
- prop.optimizedLiteral = true;
- } else {
- prop.dynamic = true;
- // check non-settable path for two-way bindings
- if ('development' !== 'production' && prop.mode === propBindingModes.TWO_WAY && !settablePathRE.test(value)) {
- prop.mode = propBindingModes.ONE_WAY;
- warn('Cannot bind two-way prop with non-settable ' + 'parent path: ' + value, vm);
- }
- }
- prop.parentPath = value;
-
- // warn required two-way
- if ('development' !== 'production' && options.twoWay && prop.mode !== propBindingModes.TWO_WAY) {
- warn('Prop "' + name + '" expects a two-way binding type.', vm);
- }
- } else if ((value = getAttr(el, attr)) !== null) {
- // has literal binding!
- prop.raw = value;
- } else if ('development' !== 'production') {
- // check possible camelCase prop usage
- var lowerCaseName = path.toLowerCase();
- value = /[A-Z\-]/.test(name) && (el.getAttribute(lowerCaseName) || el.getAttribute(':' + lowerCaseName) || el.getAttribute('v-bind:' + lowerCaseName) || el.getAttribute(':' + lowerCaseName + '.once') || el.getAttribute('v-bind:' + lowerCaseName + '.once') || el.getAttribute(':' + lowerCaseName + '.sync') || el.getAttribute('v-bind:' + lowerCaseName + '.sync'));
- if (value) {
- warn('Possible usage error for prop `' + lowerCaseName + '` - ' + 'did you mean `' + attr + '`? HTML is case-insensitive, remember to use ' + 'kebab-case for props in templates.', vm);
- } else if (options.required) {
- // warn missing required
- warn('Missing required prop: ' + name, vm);
- }
+ } else {
+ // existing directive, update
+ dir.oldValue = oldDir.value;
+ callHook$1(dir, 'update', vnode, oldVnode);
+ if (dir.def && dir.def.componentUpdated) {
+ dirsWithPostpatch.push(dir);
}
- // push prop
- props.push(prop);
}
- return makePropsLinkFn(props);
}
- /**
- * Build a function that applies props to a vm.
- *
- * @param {Array} props
- * @return {Function} propsLinkFn
- */
-
- function makePropsLinkFn(props) {
- return function propsLinkFn(vm, scope) {
- // store resolved props info
- vm._props = {};
- var inlineProps = vm.$options.propsData;
- var i = props.length;
- var prop, path, options, value, raw;
- while (i--) {
- prop = props[i];
- raw = prop.raw;
- path = prop.path;
- options = prop.options;
- vm._props[path] = prop;
- if (inlineProps && hasOwn(inlineProps, path)) {
- initProp(vm, prop, inlineProps[path]);
- }if (raw === null) {
- // initialize absent prop
- initProp(vm, prop, undefined);
- } else if (prop.dynamic) {
- // dynamic prop
- if (prop.mode === propBindingModes.ONE_TIME) {
- // one time binding
- value = (scope || vm._context || vm).$get(prop.parentPath);
- initProp(vm, prop, value);
- } else {
- if (vm._context) {
- // dynamic binding
- vm._bindDir({
- name: 'prop',
- def: propDef,
- prop: prop
- }, null, null, scope); // el, host, scope
- } else {
- // root instance
- initProp(vm, prop, vm.$get(prop.parentPath));
- }
- }
- } else if (prop.optimizedLiteral) {
- // optimized literal, cast it and just set once
- var stripped = stripQuotes(raw);
- value = stripped === raw ? toBoolean(toNumber(raw)) : stripped;
- initProp(vm, prop, value);
- } else {
- // string literal, but we need to cater for
- // Boolean props with no value, or with same
- // literal value (e.g. disabled="disabled")
- // see https://github.com/vuejs/vue-loader/issues/182
- value = options.type === Boolean && (raw === '' || raw === hyphenate(prop.name)) ? true : raw;
- initProp(vm, prop, value);
- }
- }
+ if (dirsWithInsert.length) {
+ var callInsert = function () {
+ dirsWithInsert.forEach(function (dir) {
+ callHook$1(dir, 'inserted', vnode, oldVnode);
+ });
};
+ if (isCreate) {
+ mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), 'insert', callInsert, 'dir-insert');
+ } else {
+ callInsert();
+ }
}
- /**
- * Process a prop with a rawValue, applying necessary coersions,
- * default values & assertions and call the given callback with
- * processed value.
- *
- * @param {Vue} vm
- * @param {Object} prop
- * @param {*} rawValue
- * @param {Function} fn
- */
-
- function processPropValue(vm, prop, rawValue, fn) {
- var isSimple = prop.dynamic && isSimplePath(prop.parentPath);
- var value = rawValue;
- if (value === undefined) {
- value = getPropDefaultValue(vm, prop);
- }
- value = coerceProp(prop, value, vm);
- var coerced = value !== rawValue;
- if (!assertProp(prop, value, vm)) {
- value = undefined;
- }
- if (isSimple && !coerced) {
- withoutConversion(function () {
- fn(value);
+ if (dirsWithPostpatch.length) {
+ mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), 'postpatch', function () {
+ dirsWithPostpatch.forEach(function (dir) {
+ callHook$1(dir, 'componentUpdated', vnode, oldVnode);
});
- } else {
- fn(value);
+ }, 'dir-postpatch');
+ }
+
+ if (!isCreate) {
+ for (key in oldDirs) {
+ if (!newDirs[key]) {
+ // no longer present, unbind
+ callHook$1(oldDirs[key], 'unbind', oldVnode);
+ }
}
}
+}
- /**
- * Set a prop's initial value on a vm and its data object.
- *
- * @param {Vue} vm
- * @param {Object} prop
- * @param {*} value
- */
+var emptyModifiers = Object.create(null);
- function initProp(vm, prop, value) {
- processPropValue(vm, prop, value, function (value) {
- defineReactive(vm, prop.path, value);
- });
+function normalizeDirectives$1 (
+ dirs,
+ vm
+) {
+ var res = Object.create(null);
+ if (!dirs) {
+ return res
}
+ var i, dir;
+ for (i = 0; i < dirs.length; i++) {
+ dir = dirs[i];
+ if (!dir.modifiers) {
+ dir.modifiers = emptyModifiers;
+ }
+ res[getRawDirName(dir)] = dir;
+ dir.def = resolveAsset(vm.$options, 'directives', dir.name, true);
+ }
+ return res
+}
- /**
- * Update a prop's value on a vm.
- *
- * @param {Vue} vm
- * @param {Object} prop
- * @param {*} value
- */
+function getRawDirName (dir) {
+ return dir.rawName || ((dir.name) + "." + (Object.keys(dir.modifiers || {}).join('.')))
+}
- function updateProp(vm, prop, value) {
- processPropValue(vm, prop, value, function (value) {
- vm[prop.path] = value;
- });
+function callHook$1 (dir, hook, vnode, oldVnode) {
+ var fn = dir.def && dir.def[hook];
+ if (fn) {
+ fn(vnode.elm, dir, vnode, oldVnode);
}
+}
- /**
- * Get the default value of a prop.
- *
- * @param {Vue} vm
- * @param {Object} prop
- * @return {*}
- */
+var baseModules = [
+ ref,
+ directives
+];
- function getPropDefaultValue(vm, prop) {
- // no default, return undefined
- var options = prop.options;
- if (!hasOwn(options, 'default')) {
- // absent boolean value defaults to false
- return options.type === Boolean ? false : undefined;
- }
- var def = options['default'];
- // warn against non-factory defaults for Object & Array
- if (isObject(def)) {
- 'development' !== 'production' && warn('Invalid default value for prop "' + prop.name + '": ' + 'Props with type Object/Array must use a factory function ' + 'to return the default value.', vm);
- }
- // call factory function for non-Function types
- return typeof def === 'function' && options.type !== Function ? def.call(vm) : def;
- }
+/* */
- /**
- * Assert whether a prop is valid.
- *
- * @param {Object} prop
- * @param {*} value
- * @param {Vue} vm
- */
+function updateAttrs (oldVnode, vnode) {
+ if (!oldVnode.data.attrs && !vnode.data.attrs) {
+ return
+ }
+ var key, cur, old;
+ var elm = vnode.elm;
+ var oldAttrs = oldVnode.data.attrs || {};
+ var attrs = vnode.data.attrs || {};
+ // clone observed objects, as the user probably wants to mutate it
+ if (attrs.__ob__) {
+ attrs = vnode.data.attrs = extend({}, attrs);
+ }
- function assertProp(prop, value, vm) {
- if (!prop.options.required && ( // non-required
- prop.raw === null || // abscent
- value == null) // null or undefined
- ) {
- return true;
- }
- var options = prop.options;
- var type = options.type;
- var valid = !type;
- var expectedTypes = [];
- if (type) {
- if (!isArray(type)) {
- type = [type];
- }
- for (var i = 0; i < type.length && !valid; i++) {
- var assertedType = assertType(value, type[i]);
- expectedTypes.push(assertedType.expectedType);
- valid = assertedType.valid;
- }
+ for (key in attrs) {
+ cur = attrs[key];
+ old = oldAttrs[key];
+ if (old !== cur) {
+ setAttr(elm, key, cur);
}
- if (!valid) {
- if ('development' !== 'production') {
- warn('Invalid prop: type check failed for prop "' + prop.name + '".' + ' Expected ' + expectedTypes.map(formatType).join(', ') + ', got ' + formatValue(value) + '.', vm);
- }
- return false;
- }
- var validator = options.validator;
- if (validator) {
- if (!validator(value)) {
- 'development' !== 'production' && warn('Invalid prop: custom validator check failed for prop "' + prop.name + '".', vm);
- return false;
+ }
+ for (key in oldAttrs) {
+ if (attrs[key] == null) {
+ if (isXlink(key)) {
+ elm.removeAttributeNS(xlinkNS, getXlinkProp(key));
+ } else if (!isEnumeratedAttr(key)) {
+ elm.removeAttribute(key);
}
}
- return true;
}
+}
- /**
- * Force parsing value with coerce option.
- *
- * @param {*} value
- * @param {Object} options
- * @return {*}
- */
-
- function coerceProp(prop, value, vm) {
- var coerce = prop.options.coerce;
- if (!coerce) {
- return value;
+function setAttr (el, key, value) {
+ if (isBooleanAttr(key)) {
+ // set attribute for blank value
+ // e.g. <option disabled>Select one</option>
+ if (isFalsyAttrValue(value)) {
+ el.removeAttribute(key);
+ } else {
+ el.setAttribute(key, key);
}
- if (typeof coerce === 'function') {
- return coerce(value);
+ } else if (isEnumeratedAttr(key)) {
+ el.setAttribute(key, isFalsyAttrValue(value) || value === 'false' ? 'false' : 'true');
+ } else if (isXlink(key)) {
+ if (isFalsyAttrValue(value)) {
+ el.removeAttributeNS(xlinkNS, getXlinkProp(key));
} else {
- 'development' !== 'production' && warn('Invalid coerce for prop "' + prop.name + '": expected function, got ' + typeof coerce + '.', vm);
- return value;
+ el.setAttributeNS(xlinkNS, key, value);
}
- }
-
- /**
- * Assert the type of a value
- *
- * @param {*} value
- * @param {Function} type
- * @return {Object}
- */
-
- function assertType(value, type) {
- var valid;
- var expectedType;
- if (type === String) {
- expectedType = 'string';
- valid = typeof value === expectedType;
- } else if (type === Number) {
- expectedType = 'number';
- valid = typeof value === expectedType;
- } else if (type === Boolean) {
- expectedType = 'boolean';
- valid = typeof value === expectedType;
- } else if (type === Function) {
- expectedType = 'function';
- valid = typeof value === expectedType;
- } else if (type === Object) {
- expectedType = 'object';
- valid = isPlainObject(value);
- } else if (type === Array) {
- expectedType = 'array';
- valid = isArray(value);
+ } else {
+ if (isFalsyAttrValue(value)) {
+ el.removeAttribute(key);
} else {
- valid = value instanceof type;
+ el.setAttribute(key, value);
}
- return {
- valid: valid,
- expectedType: expectedType
- };
}
+}
- /**
- * Format type for output
- *
- * @param {String} type
- * @return {String}
- */
-
- function formatType(type) {
- return type ? type.charAt(0).toUpperCase() + type.slice(1) : 'custom type';
- }
+var attrs = {
+ create: updateAttrs,
+ update: updateAttrs
+};
- /**
- * Format value
- *
- * @param {*} value
- * @return {String}
- */
+/* */
- function formatValue(val) {
- return Object.prototype.toString.call(val).slice(8, -1);
+function updateClass (oldVnode, vnode) {
+ var el = vnode.elm;
+ var data = vnode.data;
+ var oldData = oldVnode.data;
+ if (!data.staticClass && !data.class &&
+ (!oldData || (!oldData.staticClass && !oldData.class))) {
+ return
}
- var bindingModes = config._propBindingModes;
-
- var propDef = {
+ var cls = genClassForVnode(vnode);
- bind: function bind() {
- var child = this.vm;
- var parent = child._context;
- // passed in from compiler directly
- var prop = this.descriptor.prop;
- var childKey = prop.path;
- var parentKey = prop.parentPath;
- var twoWay = prop.mode === bindingModes.TWO_WAY;
-
- var parentWatcher = this.parentWatcher = new Watcher(parent, parentKey, function (val) {
- updateProp(child, prop, val);
- }, {
- twoWay: twoWay,
- filters: prop.filters,
- // important: props need to be observed on the
- // v-for scope if present
- scope: this._scope
- });
-
- // set the child initial value.
- initProp(child, prop, parentWatcher.value);
-
- // setup two-way binding
- if (twoWay) {
- // important: defer the child watcher creation until
- // the created hook (after data observation)
- var self = this;
- child.$once('pre-hook:created', function () {
- self.childWatcher = new Watcher(child, childKey, function (val) {
- parentWatcher.set(val);
- }, {
- // ensure sync upward before parent sync down.
- // this is necessary in cases e.g. the child
- // mutates a prop array, then replaces it. (#1683)
- sync: true
- });
- });
- }
- },
-
- unbind: function unbind() {
- this.parentWatcher.teardown();
- if (this.childWatcher) {
- this.childWatcher.teardown();
- }
- }
- };
-
- var queue$1 = [];
- var queued = false;
-
- /**
- * Push a job into the queue.
- *
- * @param {Function} job
- */
-
- function pushJob(job) {
- queue$1.push(job);
- if (!queued) {
- queued = true;
- nextTick(flush);
- }
+ // handle transition classes
+ var transitionClass = el._transitionClasses;
+ if (transitionClass) {
+ cls = concat(cls, stringifyClass(transitionClass));
}
- /**
- * Flush the queue, and do one forced reflow before
- * triggering transitions.
- */
-
- function flush() {
- // Force layout
- var f = document.documentElement.offsetHeight;
- for (var i = 0; i < queue$1.length; i++) {
- queue$1[i]();
- }
- queue$1 = [];
- queued = false;
- // dummy return, so js linters don't complain about
- // unused variable f
- return f;
+ // set the class
+ if (cls !== el._prevClass) {
+ el.setAttribute('class', cls);
+ el._prevClass = cls;
}
+}
- var TYPE_TRANSITION = 'transition';
- var TYPE_ANIMATION = 'animation';
- var transDurationProp = transitionProp + 'Duration';
- var animDurationProp = animationProp + 'Duration';
+var klass = {
+ create: updateClass,
+ update: updateClass
+};
- /**
- * If a just-entered element is applied the
- * leave class while its enter transition hasn't started yet,
- * and the transitioned property has the same value for both
- * enter/leave, then the leave transition will be skipped and
- * the transitionend event never fires. This function ensures
- * its callback to be called after a transition has started
- * by waiting for double raf.
- *
- * It falls back to setTimeout on devices that support CSS
- * transitions but not raf (e.g. Android 4.2 browser) - since
- * these environments are usually slow, we are giving it a
- * relatively large timeout.
- */
-
- var raf = inBrowser && window.requestAnimationFrame;
- var waitForTransitionStart = raf
- /* istanbul ignore next */
- ? function (fn) {
- raf(function () {
- raf(fn);
- });
- } : function (fn) {
- setTimeout(fn, 50);
- };
+// skip type checking this file because we need to attach private properties
+// to elements
- /**
- * A Transition object that encapsulates the state and logic
- * of the transition.
- *
- * @param {Element} el
- * @param {String} id
- * @param {Object} hooks
- * @param {Vue} vm
- */
- function Transition(el, id, hooks, vm) {
- this.id = id;
- this.el = el;
- this.enterClass = hooks && hooks.enterClass || id + '-enter';
- this.leaveClass = hooks && hooks.leaveClass || id + '-leave';
- this.hooks = hooks;
- this.vm = vm;
- // async state
- this.pendingCssEvent = this.pendingCssCb = this.cancel = this.pendingJsCb = this.op = this.cb = null;
- this.justEntered = false;
- this.entered = this.left = false;
- this.typeCache = {};
- // check css transition type
- this.type = hooks && hooks.type;
- /* istanbul ignore if */
- if ('development' !== 'production') {
- if (this.type && this.type !== TYPE_TRANSITION && this.type !== TYPE_ANIMATION) {
- warn('invalid CSS transition type for transition="' + this.id + '": ' + this.type, vm);
- }
- }
- // bind
- var self = this;['enterNextTick', 'enterDone', 'leaveNextTick', 'leaveDone'].forEach(function (m) {
- self[m] = bind(self[m], self);
- });
+function updateDOMListeners (oldVnode, vnode) {
+ if (!oldVnode.data.on && !vnode.data.on) {
+ return
}
-
- var p$1 = Transition.prototype;
-
- /**
- * Start an entering transition.
- *
- * 1. enter transition triggered
- * 2. call beforeEnter hook
- * 3. add enter class
- * 4. insert/show element
- * 5. call enter hook (with possible explicit js callback)
- * 6. reflow
- * 7. based on transition type:
- * - transition:
- * remove class now, wait for transitionend,
- * then done if there's no explicit js callback.
- * - animation:
- * wait for animationend, remove class,
- * then done if there's no explicit js callback.
- * - no css transition:
- * done now if there's no explicit js callback.
- * 8. wait for either done or js callback, then call
- * afterEnter hook.
- *
- * @param {Function} op - insert/show the element
- * @param {Function} [cb]
- */
-
- p$1.enter = function (op, cb) {
- this.cancelPending();
- this.callHook('beforeEnter');
- this.cb = cb;
- addClass(this.el, this.enterClass);
- op();
- this.entered = false;
- this.callHookWithCb('enter');
- if (this.entered) {
- return; // user called done synchronously.
- }
- this.cancel = this.hooks && this.hooks.enterCancelled;
- pushJob(this.enterNextTick);
- };
-
- /**
- * The "nextTick" phase of an entering transition, which is
- * to be pushed into a queue and executed after a reflow so
- * that removing the class can trigger a CSS transition.
- */
-
- p$1.enterNextTick = function () {
- var _this = this;
-
- // prevent transition skipping
- this.justEntered = true;
- waitForTransitionStart(function () {
- _this.justEntered = false;
- });
- var enterDone = this.enterDone;
- var type = this.getCssTransitionType(this.enterClass);
- if (!this.pendingJsCb) {
- if (type === TYPE_TRANSITION) {
- // trigger transition by removing enter class now
- removeClass(this.el, this.enterClass);
- this.setupCssCb(transitionEndEvent, enterDone);
- } else if (type === TYPE_ANIMATION) {
- this.setupCssCb(animationEndEvent, enterDone);
- } else {
- enterDone();
+ var on = vnode.data.on || {};
+ var oldOn = oldVnode.data.on || {};
+ var add = vnode.elm._v_add || (vnode.elm._v_add = function (event, handler, capture) {
+ vnode.elm.addEventListener(event, handler, capture);
+ });
+ var remove = vnode.elm._v_remove || (vnode.elm._v_remove = function (event, handler) {
+ vnode.elm.removeEventListener(event, handler);
+ });
+ updateListeners(on, oldOn, add, remove, vnode.context);
+}
+
+var events = {
+ create: updateDOMListeners,
+ update: updateDOMListeners
+};
+
+/* */
+
+function updateDOMProps (oldVnode, vnode) {
+ if (!oldVnode.data.domProps && !vnode.data.domProps) {
+ return
+ }
+ var key, cur;
+ var elm = vnode.elm;
+ var oldProps = oldVnode.data.domProps || {};
+ var props = vnode.data.domProps || {};
+ // clone observed objects, as the user probably wants to mutate it
+ if (props.__ob__) {
+ props = vnode.data.domProps = extend({}, props);
+ }
+
+ for (key in oldProps) {
+ if (props[key] == null) {
+ elm[key] = undefined;
+ }
+ }
+ for (key in props) {
+ // ignore children if the node has textContent or innerHTML,
+ // as these will throw away existing DOM nodes and cause removal errors
+ // on subsequent patches (#3360)
+ if ((key === 'textContent' || key === 'innerHTML') && vnode.children) {
+ vnode.children.length = 0;
+ }
+ cur = props[key];
+ if (key === 'value') {
+ // store value as _value as well since
+ // non-string values will be stringified
+ elm._value = cur;
+ // avoid resetting cursor position when value is the same
+ var strCur = cur == null ? '' : String(cur);
+ if (elm.value !== strCur && !elm.composing) {
+ elm.value = strCur;
}
- } else if (type === TYPE_TRANSITION) {
- removeClass(this.el, this.enterClass);
+ } else {
+ elm[key] = cur;
}
- };
+ }
+}
- /**
- * The "cleanup" phase of an entering transition.
- */
+var domProps = {
+ create: updateDOMProps,
+ update: updateDOMProps
+};
- p$1.enterDone = function () {
- this.entered = true;
- this.cancel = this.pendingJsCb = null;
- removeClass(this.el, this.enterClass);
- this.callHook('afterEnter');
- if (this.cb) this.cb();
- };
+/* */
- /**
- * Start a leaving transition.
- *
- * 1. leave transition triggered.
- * 2. call beforeLeave hook
- * 3. add leave class (trigger css transition)
- * 4. call leave hook (with possible explicit js callback)
- * 5. reflow if no explicit js callback is provided
- * 6. based on transition type:
- * - transition or animation:
- * wait for end event, remove class, then done if
- * there's no explicit js callback.
- * - no css transition:
- * done if there's no explicit js callback.
- * 7. wait for either done or js callback, then call
- * afterLeave hook.
- *
- * @param {Function} op - remove/hide the element
- * @param {Function} [cb]
- */
+var prefixes = ['Webkit', 'Moz', 'ms'];
- p$1.leave = function (op, cb) {
- this.cancelPending();
- this.callHook('beforeLeave');
- this.op = op;
- this.cb = cb;
- addClass(this.el, this.leaveClass);
- this.left = false;
- this.callHookWithCb('leave');
- if (this.left) {
- return; // user called done synchronously.
- }
- this.cancel = this.hooks && this.hooks.leaveCancelled;
- // only need to handle leaveDone if
- // 1. the transition is already done (synchronously called
- // by the user, which causes this.op set to null)
- // 2. there's no explicit js callback
- if (this.op && !this.pendingJsCb) {
- // if a CSS transition leaves immediately after enter,
- // the transitionend event never fires. therefore we
- // detect such cases and end the leave immediately.
- if (this.justEntered) {
- this.leaveDone();
- } else {
- pushJob(this.leaveNextTick);
- }
+var testEl;
+var normalize = cached(function (prop) {
+ testEl = testEl || document.createElement('div');
+ prop = camelize(prop);
+ if (prop !== 'filter' && (prop in testEl.style)) {
+ return prop
+ }
+ var upper = prop.charAt(0).toUpperCase() + prop.slice(1);
+ for (var i = 0; i < prefixes.length; i++) {
+ var prefixed = prefixes[i] + upper;
+ if (prefixed in testEl.style) {
+ return prefixed
}
- };
+ }
+});
- /**
- * The "nextTick" phase of a leaving transition.
- */
+function updateStyle (oldVnode, vnode) {
+ if ((!oldVnode.data || !oldVnode.data.style) && !vnode.data.style) {
+ return
+ }
+ var cur, name;
+ var el = vnode.elm;
+ var oldStyle = oldVnode.data.style || {};
+ var style = vnode.data.style || {};
- p$1.leaveNextTick = function () {
- var type = this.getCssTransitionType(this.leaveClass);
- if (type) {
- var event = type === TYPE_TRANSITION ? transitionEndEvent : animationEndEvent;
- this.setupCssCb(event, this.leaveDone);
- } else {
- this.leaveDone();
- }
- };
+ // handle string
+ if (typeof style === 'string') {
+ el.style.cssText = style;
+ return
+ }
- /**
- * The "cleanup" phase of a leaving transition.
- */
+ var needClone = style.__ob__;
- p$1.leaveDone = function () {
- this.left = true;
- this.cancel = this.pendingJsCb = null;
- this.op();
- removeClass(this.el, this.leaveClass);
- this.callHook('afterLeave');
- if (this.cb) this.cb();
- this.op = null;
- };
+ // handle array syntax
+ if (Array.isArray(style)) {
+ style = vnode.data.style = toObject(style);
+ }
- /**
- * Cancel any pending callbacks from a previously running
- * but not finished transition.
- */
+ // clone the style for future updates,
+ // in case the user mutates the style object in-place.
+ if (needClone) {
+ style = vnode.data.style = extend({}, style);
+ }
- p$1.cancelPending = function () {
- this.op = this.cb = null;
- var hasPending = false;
- if (this.pendingCssCb) {
- hasPending = true;
- off(this.el, this.pendingCssEvent, this.pendingCssCb);
- this.pendingCssEvent = this.pendingCssCb = null;
- }
- if (this.pendingJsCb) {
- hasPending = true;
- this.pendingJsCb.cancel();
- this.pendingJsCb = null;
+ for (name in oldStyle) {
+ if (style[name] == null) {
+ el.style[normalize(name)] = '';
}
- if (hasPending) {
- removeClass(this.el, this.enterClass);
- removeClass(this.el, this.leaveClass);
- }
- if (this.cancel) {
- this.cancel.call(this.vm, this.el);
- this.cancel = null;
- }
- };
-
- /**
- * Call a user-provided synchronous hook function.
- *
- * @param {String} type
- */
-
- p$1.callHook = function (type) {
- if (this.hooks && this.hooks[type]) {
- this.hooks[type].call(this.vm, this.el);
+ }
+ for (name in style) {
+ cur = style[name];
+ if (cur !== oldStyle[name]) {
+ // ie9 setting to null has no effect, must use empty string
+ el.style[normalize(name)] = cur == null ? '' : cur;
}
- };
-
- /**
- * Call a user-provided, potentially-async hook function.
- * We check for the length of arguments to see if the hook
- * expects a `done` callback. If true, the transition's end
- * will be determined by when the user calls that callback;
- * otherwise, the end is determined by the CSS transition or
- * animation.
- *
- * @param {String} type
- */
+ }
+}
- p$1.callHookWithCb = function (type) {
- var hook = this.hooks && this.hooks[type];
- if (hook) {
- if (hook.length > 1) {
- this.pendingJsCb = cancellable(this[type + 'Done']);
- }
- hook.call(this.vm, this.el, this.pendingJsCb);
- }
- };
+var style = {
+ create: updateStyle,
+ update: updateStyle
+};
- /**
- * Get an element's transition type based on the
- * calculated styles.
- *
- * @param {String} className
- * @return {Number}
- */
+/* */
- p$1.getCssTransitionType = function (className) {
- /* istanbul ignore if */
- if (!transitionEndEvent ||
- // skip CSS transitions if page is not visible -
- // this solves the issue of transitionend events not
- // firing until the page is visible again.
- // pageVisibility API is supported in IE10+, same as
- // CSS transitions.
- document.hidden ||
- // explicit js-only transition
- this.hooks && this.hooks.css === false ||
- // element is hidden
- isHidden(this.el)) {
- return;
- }
- var type = this.type || this.typeCache[className];
- if (type) return type;
- var inlineStyles = this.el.style;
- var computedStyles = window.getComputedStyle(this.el);
- var transDuration = inlineStyles[transDurationProp] || computedStyles[transDurationProp];
- if (transDuration && transDuration !== '0s') {
- type = TYPE_TRANSITION;
+/**
+ * Add class with compatibility for SVG since classList is not supported on
+ * SVG elements in IE
+ */
+function addClass (el, cls) {
+ /* istanbul ignore else */
+ if (el.classList) {
+ if (cls.indexOf(' ') > -1) {
+ cls.split(/\s+/).forEach(function (c) { return el.classList.add(c); });
} else {
- var animDuration = inlineStyles[animDurationProp] || computedStyles[animDurationProp];
- if (animDuration && animDuration !== '0s') {
- type = TYPE_ANIMATION;
- }
+ el.classList.add(cls);
}
- if (type) {
- this.typeCache[className] = type;
+ } else {
+ var cur = ' ' + el.getAttribute('class') + ' ';
+ if (cur.indexOf(' ' + cls + ' ') < 0) {
+ el.setAttribute('class', (cur + cls).trim());
}
- return type;
- };
-
- /**
- * Setup a CSS transitionend/animationend callback.
- *
- * @param {String} event
- * @param {Function} cb
- */
-
- p$1.setupCssCb = function (event, cb) {
- this.pendingCssEvent = event;
- var self = this;
- var el = this.el;
- var onEnd = this.pendingCssCb = function (e) {
- if (e.target === el) {
- off(el, event, onEnd);
- self.pendingCssEvent = self.pendingCssCb = null;
- if (!self.pendingJsCb && cb) {
- cb();
- }
- }
- };
- on(el, event, onEnd);
- };
-
- /**
- * Check if an element is hidden - in that case we can just
- * skip the transition alltogether.
- *
- * @param {Element} el
- * @return {Boolean}
- */
+ }
+}
- function isHidden(el) {
- if (/svg$/.test(el.namespaceURI)) {
- // SVG elements do not have offset(Width|Height)
- // so we need to check the client rect
- var rect = el.getBoundingClientRect();
- return !(rect.width || rect.height);
+/**
+ * Remove class with compatibility for SVG since classList is not supported on
+ * SVG elements in IE
+ */
+function removeClass (el, cls) {
+ /* istanbul ignore else */
+ if (el.classList) {
+ if (cls.indexOf(' ') > -1) {
+ cls.split(/\s+/).forEach(function (c) { return el.classList.remove(c); });
} else {
- return !(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
+ el.classList.remove(cls);
+ }
+ } else {
+ var cur = ' ' + el.getAttribute('class') + ' ';
+ var tar = ' ' + cls + ' ';
+ while (cur.indexOf(tar) >= 0) {
+ cur = cur.replace(tar, ' ');
}
+ el.setAttribute('class', cur.trim());
}
+}
- var transition$1 = {
+/* */
- priority: TRANSITION,
+var hasTransition = inBrowser && !isIE9;
+var TRANSITION = 'transition';
+var ANIMATION = 'animation';
- update: function update(id, oldId) {
- var el = this.el;
- // resolve on owner vm
- var hooks = resolveAsset(this.vm.$options, 'transitions', id);
- id = id || 'v';
- oldId = oldId || 'v';
- el.__v_trans = new Transition(el, id, hooks, this.vm);
- removeClass(el, oldId + '-transition');
- addClass(el, id + '-transition');
- }
+// Transition property/event sniffing
+var transitionProp = 'transition';
+var transitionEndEvent = 'transitionend';
+var animationProp = 'animation';
+var animationEndEvent = 'animationend';
+if (hasTransition) {
+ /* istanbul ignore if */
+ if (window.ontransitionend === undefined &&
+ window.onwebkittransitionend !== undefined) {
+ transitionProp = 'WebkitTransition';
+ transitionEndEvent = 'webkitTransitionEnd';
+ }
+ if (window.onanimationend === undefined &&
+ window.onwebkitanimationend !== undefined) {
+ animationProp = 'WebkitAnimation';
+ animationEndEvent = 'webkitAnimationEnd';
+ }
+}
+
+var raf = (inBrowser && window.requestAnimationFrame) || setTimeout;
+function nextFrame (fn) {
+ raf(function () {
+ raf(fn);
+ });
+}
+
+function addTransitionClass (el, cls) {
+ (el._transitionClasses || (el._transitionClasses = [])).push(cls);
+ addClass(el, cls);
+}
+
+function removeTransitionClass (el, cls) {
+ if (el._transitionClasses) {
+ remove$1(el._transitionClasses, cls);
+ }
+ removeClass(el, cls);
+}
+
+function whenTransitionEnds (
+ el,
+ expectedType,
+ cb
+) {
+ var ref = getTransitionInfo(el, expectedType);
+ var type = ref.type;
+ var timeout = ref.timeout;
+ var propCount = ref.propCount;
+ if (!type) { return cb() }
+ var event = type === TRANSITION ? transitionEndEvent : animationEndEvent;
+ var ended = 0;
+ var end = function () {
+ el.removeEventListener(event, onEnd);
+ cb();
};
-
- var internalDirectives = {
- style: style,
- 'class': vClass,
- component: component,
- prop: propDef,
- transition: transition$1
+ var onEnd = function (e) {
+ if (e.target === el) {
+ if (++ended >= propCount) {
+ end();
+ }
+ }
};
-
- // special binding prefixes
- var bindRE = /^v-bind:|^:/;
- var onRE = /^v-on:|^@/;
- var dirAttrRE = /^v-([^:]+)(?:$|:(.*)$)/;
- var modifierRE = /\.[^\.]+/g;
- var transitionRE = /^(v-bind:|:)?transition$/;
-
- // default directive priority
- var DEFAULT_PRIORITY = 1000;
- var DEFAULT_TERMINAL_PRIORITY = 2000;
-
- /**
- * Compile a template and return a reusable composite link
- * function, which recursively contains more link functions
- * inside. This top level compile function would normally
- * be called on instance root nodes, but can also be used
- * for partial compilation if the partial argument is true.
- *
- * The returned composite link function, when called, will
- * return an unlink function that tearsdown all directives
- * created during the linking phase.
- *
- * @param {Element|DocumentFragment} el
- * @param {Object} options
- * @param {Boolean} partial
- * @return {Function}
- */
-
- function compile(el, options, partial) {
- // link function for the node itself.
- var nodeLinkFn = partial || !options._asComponent ? compileNode(el, options) : null;
- // link function for the childNodes
- var childLinkFn = !(nodeLinkFn && nodeLinkFn.terminal) && !isScript(el) && el.hasChildNodes() ? compileNodeList(el.childNodes, options) : null;
-
- /**
- * A composite linker function to be called on a already
- * compiled piece of DOM, which instantiates all directive
- * instances.
- *
- * @param {Vue} vm
- * @param {Element|DocumentFragment} el
- * @param {Vue} [host] - host vm of transcluded content
- * @param {Object} [scope] - v-for scope
- * @param {Fragment} [frag] - link context fragment
- * @return {Function|undefined}
- */
-
- return function compositeLinkFn(vm, el, host, scope, frag) {
- // cache childNodes before linking parent, fix #657
- var childNodes = toArray(el.childNodes);
- // link
- var dirs = linkAndCapture(function compositeLinkCapturer() {
- if (nodeLinkFn) nodeLinkFn(vm, el, host, scope, frag);
- if (childLinkFn) childLinkFn(vm, childNodes, host, scope, frag);
- }, vm);
- return makeUnlinkFn(vm, dirs);
- };
- }
-
- /**
- * Apply a linker to a vm/element pair and capture the
- * directives created during the process.
- *
- * @param {Function} linker
- * @param {Vue} vm
- */
-
- function linkAndCapture(linker, vm) {
- /* istanbul ignore if */
- if ('development' === 'production') {}
- var originalDirCount = vm._directives.length;
- linker();
- var dirs = vm._directives.slice(originalDirCount);
- dirs.sort(directiveComparator);
- for (var i = 0, l = dirs.length; i < l; i++) {
- dirs[i]._bind();
+ setTimeout(function () {
+ if (ended < propCount) {
+ end();
+ }
+ }, timeout + 1);
+ el.addEventListener(event, onEnd);
+}
+
+var transformRE = /\b(transform|all)(,|$)/;
+
+function getTransitionInfo (el, expectedType) {
+ var styles = window.getComputedStyle(el);
+ var transitioneDelays = styles[transitionProp + 'Delay'].split(', ');
+ var transitionDurations = styles[transitionProp + 'Duration'].split(', ');
+ var transitionTimeout = getTimeout(transitioneDelays, transitionDurations);
+ var animationDelays = styles[animationProp + 'Delay'].split(', ');
+ var animationDurations = styles[animationProp + 'Duration'].split(', ');
+ var animationTimeout = getTimeout(animationDelays, animationDurations);
+
+ var type;
+ var timeout = 0;
+ var propCount = 0;
+ /* istanbul ignore if */
+ if (expectedType === TRANSITION) {
+ if (transitionTimeout > 0) {
+ type = TRANSITION;
+ timeout = transitionTimeout;
+ propCount = transitionDurations.length;
+ }
+ } else if (expectedType === ANIMATION) {
+ if (animationTimeout > 0) {
+ type = ANIMATION;
+ timeout = animationTimeout;
+ propCount = animationDurations.length;
}
- return dirs;
- }
-
- /**
- * Directive priority sort comparator
- *
- * @param {Object} a
- * @param {Object} b
- */
-
- function directiveComparator(a, b) {
- a = a.descriptor.def.priority || DEFAULT_PRIORITY;
- b = b.descriptor.def.priority || DEFAULT_PRIORITY;
- return a > b ? -1 : a === b ? 0 : 1;
+ } else {
+ timeout = Math.max(transitionTimeout, animationTimeout);
+ type = timeout > 0
+ ? transitionTimeout > animationTimeout
+ ? TRANSITION
+ : ANIMATION
+ : null;
+ propCount = type
+ ? type === TRANSITION
+ ? transitionDurations.length
+ : animationDurations.length
+ : 0;
+ }
+ var hasTransform =
+ type === TRANSITION &&
+ transformRE.test(styles[transitionProp + 'Property']);
+ return {
+ type: type,
+ timeout: timeout,
+ propCount: propCount,
+ hasTransform: hasTransform
+ }
+}
+
+function getTimeout (delays, durations) {
+ return Math.max.apply(null, durations.map(function (d, i) {
+ return toMs(d) + toMs(delays[i])
+ }))
+}
+
+function toMs (s) {
+ return Number(s.slice(0, -1)) * 1000
+}
+
+/* */
+
+function enter (vnode) {
+ var el = vnode.elm;
+
+ // call leave callback now
+ if (el._leaveCb) {
+ el._leaveCb.cancelled = true;
+ el._leaveCb();
+ }
+
+ var data = resolveTransition(vnode.data.transition);
+ if (!data) {
+ return
}
- /**
- * Linker functions return an unlink function that
- * tearsdown all directives instances generated during
- * the process.
- *
- * We create unlink functions with only the necessary
- * information to avoid retaining additional closures.
- *
- * @param {Vue} vm
- * @param {Array} dirs
- * @param {Vue} [context]
- * @param {Array} [contextDirs]
- * @return {Function}
- */
+ /* istanbul ignore if */
+ if (el._enterCb || el.nodeType !== 1) {
+ return
+ }
+
+ var css = data.css;
+ var type = data.type;
+ var enterClass = data.enterClass;
+ var enterActiveClass = data.enterActiveClass;
+ var appearClass = data.appearClass;
+ var appearActiveClass = data.appearActiveClass;
+ var beforeEnter = data.beforeEnter;
+ var enter = data.enter;
+ var afterEnter = data.afterEnter;
+ var enterCancelled = data.enterCancelled;
+ var beforeAppear = data.beforeAppear;
+ var appear = data.appear;
+ var afterAppear = data.afterAppear;
+ var appearCancelled = data.appearCancelled;
+
+ // activeInstance will always be the <transition> component managing this
+ // transition. One edge case to check is when the <transition> is placed
+ // as the root node of a child component. In that case we need to check
+ // <transition>'s parent for appear check.
+ var transitionNode = activeInstance.$vnode;
+ var context = transitionNode && transitionNode.parent
+ ? transitionNode.parent.context
+ : activeInstance;
+
+ var isAppear = !context._isMounted || !vnode.isRootInsert;
+
+ if (isAppear && !appear && appear !== '') {
+ return
+ }
+
+ var startClass = isAppear ? appearClass : enterClass;
+ var activeClass = isAppear ? appearActiveClass : enterActiveClass;
+ var beforeEnterHook = isAppear ? (beforeAppear || beforeEnter) : beforeEnter;
+ var enterHook = isAppear ? (typeof appear === 'function' ? appear : enter) : enter;
+ var afterEnterHook = isAppear ? (afterAppear || afterEnter) : afterEnter;
+ var enterCancelledHook = isAppear ? (appearCancelled || enterCancelled) : enterCancelled;
+
+ var expectsCSS = css !== false && !isIE9;
+ var userWantsControl =
+ enterHook &&
+ // enterHook may be a bound method which exposes
+ // the length of original fn as _length
+ (enterHook._length || enterHook.length) > 1;
+
+ var cb = el._enterCb = once(function () {
+ if (expectsCSS) {
+ removeTransitionClass(el, activeClass);
+ }
+ if (cb.cancelled) {
+ if (expectsCSS) {
+ removeTransitionClass(el, startClass);
+ }
+ enterCancelledHook && enterCancelledHook(el);
+ } else {
+ afterEnterHook && afterEnterHook(el);
+ }
+ el._enterCb = null;
+ });
- function makeUnlinkFn(vm, dirs, context, contextDirs) {
- function unlink(destroying) {
- teardownDirs(vm, dirs, destroying);
- if (context && contextDirs) {
- teardownDirs(context, contextDirs);
+ if (!vnode.data.show) {
+ // remove pending leave element on enter by injecting an insert hook
+ mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), 'insert', function () {
+ var parent = el.parentNode;
+ var pendingNode = parent && parent._pending && parent._pending[vnode.key];
+ if (pendingNode && pendingNode.tag === vnode.tag && pendingNode.elm._leaveCb) {
+ pendingNode.elm._leaveCb();
}
- }
- // expose linked directives
- unlink.dirs = dirs;
- return unlink;
+ enterHook && enterHook(el, cb);
+ }, 'transition-insert');
}
- /**
- * Teardown partial linked directives.
- *
- * @param {Vue} vm
- * @param {Array} dirs
- * @param {Boolean} destroying
- */
-
- function teardownDirs(vm, dirs, destroying) {
- var i = dirs.length;
- while (i--) {
- dirs[i]._teardown();
- if ('development' !== 'production' && !destroying) {
- vm._directives.$remove(dirs[i]);
+ // start enter transition
+ beforeEnterHook && beforeEnterHook(el);
+ if (expectsCSS) {
+ addTransitionClass(el, startClass);
+ addTransitionClass(el, activeClass);
+ nextFrame(function () {
+ removeTransitionClass(el, startClass);
+ if (!cb.cancelled && !userWantsControl) {
+ whenTransitionEnds(el, type, cb);
}
- }
+ });
}
- /**
- * Compile link props on an instance.
- *
- * @param {Vue} vm
- * @param {Element} el
- * @param {Object} props
- * @param {Object} [scope]
- * @return {Function}
- */
-
- function compileAndLinkProps(vm, el, props, scope) {
- var propsLinkFn = compileProps(el, props, vm);
- var propDirs = linkAndCapture(function () {
- propsLinkFn(vm, scope);
- }, vm);
- return makeUnlinkFn(vm, propDirs);
+ if (vnode.data.show) {
+ enterHook && enterHook(el, cb);
}
- /**
- * Compile the root element of an instance.
- *
- * 1. attrs on context container (context scope)
- * 2. attrs on the component template root node, if
- * replace:true (child scope)
- *
- * If this is a fragment instance, we only need to compile 1.
- *
- * @param {Element} el
- * @param {Object} options
- * @param {Object} contextOptions
- * @return {Function}
- */
-
- function compileRoot(el, options, contextOptions) {
- var containerAttrs = options._containerAttrs;
- var replacerAttrs = options._replacerAttrs;
- var contextLinkFn, replacerLinkFn;
-
- // only need to compile other attributes for
- // non-fragment instances
- if (el.nodeType !== 11) {
- // for components, container and replacer need to be
- // compiled separately and linked in different scopes.
- if (options._asComponent) {
- // 2. container attributes
- if (containerAttrs && contextOptions) {
- contextLinkFn = compileDirectives(containerAttrs, contextOptions);
- }
- if (replacerAttrs) {
- // 3. replacer attributes
- replacerLinkFn = compileDirectives(replacerAttrs, options);
- }
- } else {
- // non-component, just compile as a normal element.
- replacerLinkFn = compileDirectives(el.attributes, options);
- }
- } else if ('development' !== 'production' && containerAttrs) {
- // warn container directives for fragment instances
- var names = containerAttrs.filter(function (attr) {
- // allow vue-loader/vueify scoped css attributes
- return attr.name.indexOf('_v-') < 0 &&
- // allow event listeners
- !onRE.test(attr.name) &&
- // allow slots
- attr.name !== 'slot';
- }).map(function (attr) {
- return '"' + attr.name + '"';
- });
- if (names.length) {
- var plural = names.length > 1;
- warn('Attribute' + (plural ? 's ' : ' ') + names.join(', ') + (plural ? ' are' : ' is') + ' ignored on component ' + '<' + options.el.tagName.toLowerCase() + '> because ' + 'the component is a fragment instance: ' + 'http://vuejs.org/guide/components.html#Fragment-Instance');
- }
- }
-
- options._containerAttrs = options._replacerAttrs = null;
- return function rootLinkFn(vm, el, scope) {
- // link context scope dirs
- var context = vm._context;
- var contextDirs;
- if (context && contextLinkFn) {
- contextDirs = linkAndCapture(function () {
- contextLinkFn(context, el, null, scope);
- }, context);
- }
+ if (!expectsCSS && !userWantsControl) {
+ cb();
+ }
+}
- // link self
- var selfDirs = linkAndCapture(function () {
- if (replacerLinkFn) replacerLinkFn(vm, el);
- }, vm);
+function leave (vnode, rm) {
+ var el = vnode.elm;
- // return the unlink function that tearsdown context
- // container directives.
- return makeUnlinkFn(vm, selfDirs, context, contextDirs);
- };
+ // call enter callback now
+ if (el._enterCb) {
+ el._enterCb.cancelled = true;
+ el._enterCb();
}
- /**
- * Compile a node and return a nodeLinkFn based on the
- * node type.
- *
- * @param {Node} node
- * @param {Object} options
- * @return {Function|null}
- */
+ var data = resolveTransition(vnode.data.transition);
+ if (!data) {
+ return rm()
+ }
- function compileNode(node, options) {
- var type = node.nodeType;
- if (type === 1 && !isScript(node)) {
- return compileElement(node, options);
- } else if (type === 3 && node.data.trim()) {
- return compileTextNode(node, options);
+ /* istanbul ignore if */
+ if (el._leaveCb || el.nodeType !== 1) {
+ return
+ }
+
+ var css = data.css;
+ var type = data.type;
+ var leaveClass = data.leaveClass;
+ var leaveActiveClass = data.leaveActiveClass;
+ var beforeLeave = data.beforeLeave;
+ var leave = data.leave;
+ var afterLeave = data.afterLeave;
+ var leaveCancelled = data.leaveCancelled;
+ var delayLeave = data.delayLeave;
+
+ var expectsCSS = css !== false && !isIE9;
+ var userWantsControl =
+ leave &&
+ // leave hook may be a bound method which exposes
+ // the length of original fn as _length
+ (leave._length || leave.length) > 1;
+
+ var cb = el._leaveCb = once(function () {
+ if (el.parentNode && el.parentNode._pending) {
+ el.parentNode._pending[vnode.key] = null;
+ }
+ if (expectsCSS) {
+ removeTransitionClass(el, leaveActiveClass);
+ }
+ if (cb.cancelled) {
+ if (expectsCSS) {
+ removeTransitionClass(el, leaveClass);
+ }
+ leaveCancelled && leaveCancelled(el);
} else {
- return null;
+ rm();
+ afterLeave && afterLeave(el);
}
- }
+ el._leaveCb = null;
+ });
- /**
- * Compile an element and return a nodeLinkFn.
- *
- * @param {Element} el
- * @param {Object} options
- * @return {Function|null}
- */
+ if (delayLeave) {
+ delayLeave(performLeave);
+ } else {
+ performLeave();
+ }
- function compileElement(el, options) {
- // preprocess textareas.
- // textarea treats its text content as the initial value.
- // just bind it as an attr directive for value.
- if (el.tagName === 'TEXTAREA') {
- var tokens = parseText(el.value);
- if (tokens) {
- el.setAttribute(':value', tokensToExp(tokens));
- el.value = '';
- }
- }
- var linkFn;
- var hasAttrs = el.hasAttributes();
- var attrs = hasAttrs && toArray(el.attributes);
- // check terminal directives (for & if)
- if (hasAttrs) {
- linkFn = checkTerminalDirectives(el, attrs, options);
+ function performLeave () {
+ // the delayed leave may have already been cancelled
+ if (cb.cancelled) {
+ return
}
- // check element directives
- if (!linkFn) {
- linkFn = checkElementDirectives(el, options);
+ // record leaving element
+ if (!vnode.data.show) {
+ (el.parentNode._pending || (el.parentNode._pending = {}))[vnode.key] = vnode;
}
- // check component
- if (!linkFn) {
- linkFn = checkComponent(el, options);
+ beforeLeave && beforeLeave(el);
+ if (expectsCSS) {
+ addTransitionClass(el, leaveClass);
+ addTransitionClass(el, leaveActiveClass);
+ nextFrame(function () {
+ removeTransitionClass(el, leaveClass);
+ if (!cb.cancelled && !userWantsControl) {
+ whenTransitionEnds(el, type, cb);
+ }
+ });
}
- // normal directives
- if (!linkFn && hasAttrs) {
- linkFn = compileDirectives(attrs, options);
+ leave && leave(el, cb);
+ if (!expectsCSS && !userWantsControl) {
+ cb();
}
- return linkFn;
}
+}
- /**
- * Compile a textNode and return a nodeLinkFn.
- *
- * @param {TextNode} node
- * @param {Object} options
- * @return {Function|null} textNodeLinkFn
- */
-
- function compileTextNode(node, options) {
- // skip marked text nodes
- if (node._skip) {
- return removeText;
- }
-
- var tokens = parseText(node.wholeText);
- if (!tokens) {
- return null;
- }
-
- // mark adjacent text nodes as skipped,
- // because we are using node.wholeText to compile
- // all adjacent text nodes together. This fixes
- // issues in IE where sometimes it splits up a single
- // text node into multiple ones.
- var next = node.nextSibling;
- while (next && next.nodeType === 3) {
- next._skip = true;
- next = next.nextSibling;
- }
-
- var frag = document.createDocumentFragment();
- var el, token;
- for (var i = 0, l = tokens.length; i < l; i++) {
- token = tokens[i];
- el = token.tag ? processTextToken(token, options) : document.createTextNode(token.value);
- frag.appendChild(el);
+function resolveTransition (def$$1) {
+ if (!def$$1) {
+ return
+ }
+ /* istanbul ignore else */
+ if (typeof def$$1 === 'object') {
+ var res = {};
+ if (def$$1.css !== false) {
+ extend(res, autoCssTransition(def$$1.name || 'v'));
}
- return makeTextNodeLinkFn(tokens, frag, options);
+ extend(res, def$$1);
+ return res
+ } else if (typeof def$$1 === 'string') {
+ return autoCssTransition(def$$1)
}
+}
- /**
- * Linker for an skipped text node.
- *
- * @param {Vue} vm
- * @param {Text} node
- */
-
- function removeText(vm, node) {
- remove(node);
+var autoCssTransition = cached(function (name) {
+ return {
+ enterClass: (name + "-enter"),
+ leaveClass: (name + "-leave"),
+ appearClass: (name + "-enter"),
+ enterActiveClass: (name + "-enter-active"),
+ leaveActiveClass: (name + "-leave-active"),
+ appearActiveClass: (name + "-enter-active")
}
+});
- /**
- * Process a single text token.
- *
- * @param {Object} token
- * @param {Object} options
- * @return {Node}
- */
+function once (fn) {
+ var called = false;
+ return function () {
+ if (!called) {
+ called = true;
+ fn();
+ }
+ }
+}
- function processTextToken(token, options) {
- var el;
- if (token.oneTime) {
- el = document.createTextNode(token.value);
+var transition = inBrowser ? {
+ create: function create (_, vnode) {
+ if (!vnode.data.show) {
+ enter(vnode);
+ }
+ },
+ remove: function remove (vnode, rm) {
+ /* istanbul ignore else */
+ if (!vnode.data.show) {
+ leave(vnode, rm);
} else {
- if (token.html) {
- el = document.createComment('v-html');
- setTokenType('html');
- } else {
- // IE will clean up empty textNodes during
- // frag.cloneNode(true), so we have to give it
- // something here...
- el = document.createTextNode(' ');
- setTokenType('text');
- }
- }
- function setTokenType(type) {
- if (token.descriptor) return;
- var parsed = parseDirective(token.value);
- token.descriptor = {
- name: type,
- def: directives[type],
- expression: parsed.expression,
- filters: parsed.filters
- };
+ rm();
}
- return el;
}
+} : {};
- /**
- * Build a function that processes a textNode.
- *
- * @param {Array<Object>} tokens
- * @param {DocumentFragment} frag
- */
-
- function makeTextNodeLinkFn(tokens, frag) {
- return function textNodeLinkFn(vm, el, host, scope) {
- var fragClone = frag.cloneNode(true);
- var childNodes = toArray(fragClone.childNodes);
- var token, value, node;
- for (var i = 0, l = tokens.length; i < l; i++) {
- token = tokens[i];
- value = token.value;
- if (token.tag) {
- node = childNodes[i];
- if (token.oneTime) {
- value = (scope || vm).$eval(value);
- if (token.html) {
- replace(node, parseTemplate(value, true));
- } else {
- node.data = _toString(value);
- }
- } else {
- vm._bindDir(token.descriptor, node, host, scope);
- }
- }
- }
- replace(el, fragClone);
- };
- }
+var platformModules = [
+ attrs,
+ klass,
+ events,
+ domProps,
+ style,
+ transition
+];
- /**
- * Compile a node list and return a childLinkFn.
- *
- * @param {NodeList} nodeList
- * @param {Object} options
- * @return {Function|undefined}
- */
+/* */
- function compileNodeList(nodeList, options) {
- var linkFns = [];
- var nodeLinkFn, childLinkFn, node;
- for (var i = 0, l = nodeList.length; i < l; i++) {
- node = nodeList[i];
- nodeLinkFn = compileNode(node, options);
- childLinkFn = !(nodeLinkFn && nodeLinkFn.terminal) && node.tagName !== 'SCRIPT' && node.hasChildNodes() ? compileNodeList(node.childNodes, options) : null;
- linkFns.push(nodeLinkFn, childLinkFn);
- }
- return linkFns.length ? makeChildLinkFn(linkFns) : null;
- }
+// the directive module should be applied last, after all
+// built-in modules have been applied.
+var modules = platformModules.concat(baseModules);
- /**
- * Make a child link function for a node's childNodes.
- *
- * @param {Array<Function>} linkFns
- * @return {Function} childLinkFn
- */
+var patch$1 = createPatchFunction({ nodeOps: nodeOps, modules: modules });
- function makeChildLinkFn(linkFns) {
- return function childLinkFn(vm, nodes, host, scope, frag) {
- var node, nodeLinkFn, childrenLinkFn;
- for (var i = 0, n = 0, l = linkFns.length; i < l; n++) {
- node = nodes[n];
- nodeLinkFn = linkFns[i++];
- childrenLinkFn = linkFns[i++];
- // cache childNodes before linking parent, fix #657
- var childNodes = toArray(node.childNodes);
- if (nodeLinkFn) {
- nodeLinkFn(vm, node, host, scope, frag);
- }
- if (childrenLinkFn) {
- childrenLinkFn(vm, childNodes, host, scope, frag);
- }
- }
- };
- }
+/**
+ * Not type checking this file because flow doesn't like attaching
+ * properties to Elements.
+ */
- /**
- * Check for element directives (custom elements that should
- * be resovled as terminal directives).
- *
- * @param {Element} el
- * @param {Object} options
- */
+var modelableTagRE = /^input|select|textarea|vue-component-[0-9]+(-[0-9a-zA-Z_\-]*)?$/;
- function checkElementDirectives(el, options) {
- var tag = el.tagName.toLowerCase();
- if (commonTagRE.test(tag)) {
- return;
+/* istanbul ignore if */
+if (isIE9) {
+ // http://www.matts411.com/post/internet-explorer-9-oninput/
+ document.addEventListener('selectionchange', function () {
+ var el = document.activeElement;
+ if (el && el.vmodel) {
+ trigger(el, 'input');
}
- var def = resolveAsset(options, 'elementDirectives', tag);
- if (def) {
- return makeTerminalNodeLinkFn(el, tag, '', options, def);
+ });
+}
+
+var model = {
+ inserted: function inserted (el, binding, vnode) {
+ {
+ if (!modelableTagRE.test(vnode.tag)) {
+ warn(
+ "v-model is not supported on element type: <" + (vnode.tag) + ">. " +
+ 'If you are working with contenteditable, it\'s recommended to ' +
+ 'wrap a library dedicated for that purpose inside a custom component.',
+ vnode.context
+ );
+ }
}
- }
-
- /**
- * Check if an element is a component. If yes, return
- * a component link function.
- *
- * @param {Element} el
- * @param {Object} options
- * @return {Function|undefined}
- */
-
- function checkComponent(el, options) {
- var component = checkComponentAttr(el, options);
- if (component) {
- var ref = findRef(el);
- var descriptor = {
- name: 'component',
- ref: ref,
- expression: component.id,
- def: internalDirectives.component,
- modifiers: {
- literal: !component.dynamic
- }
- };
- var componentLinkFn = function componentLinkFn(vm, el, host, scope, frag) {
- if (ref) {
- defineReactive((scope || vm).$refs, ref, null);
- }
- vm._bindDir(descriptor, el, host, scope, frag);
+ if (vnode.tag === 'select') {
+ var cb = function () {
+ setSelected(el, binding, vnode.context);
};
- componentLinkFn.terminal = true;
- return componentLinkFn;
- }
- }
-
- /**
- * Check an element for terminal directives in fixed order.
- * If it finds one, return a terminal link function.
- *
- * @param {Element} el
- * @param {Array} attrs
- * @param {Object} options
- * @return {Function} terminalLinkFn
- */
-
- function checkTerminalDirectives(el, attrs, options) {
- // skip v-pre
- if (getAttr(el, 'v-pre') !== null) {
- return skip;
- }
- // skip v-else block, but only if following v-if
- if (el.hasAttribute('v-else')) {
- var prev = el.previousElementSibling;
- if (prev && prev.hasAttribute('v-if')) {
- return skip;
- }
- }
-
- var attr, name, value, modifiers, matched, dirName, rawName, arg, def, termDef;
- for (var i = 0, j = attrs.length; i < j; i++) {
- attr = attrs[i];
- name = attr.name.replace(modifierRE, '');
- if (matched = name.match(dirAttrRE)) {
- def = resolveAsset(options, 'directives', matched[1]);
- if (def && def.terminal) {
- if (!termDef || (def.priority || DEFAULT_TERMINAL_PRIORITY) > termDef.priority) {
- termDef = def;
- rawName = attr.name;
- modifiers = parseModifiers(attr.name);
- value = attr.value;
- dirName = matched[1];
- arg = matched[2];
- }
+ cb();
+ /* istanbul ignore if */
+ if (isIE || isEdge) {
+ setTimeout(cb, 0);
+ }
+ } else if (
+ (vnode.tag === 'textarea' || el.type === 'text') &&
+ !binding.modifiers.lazy
+ ) {
+ if (!isAndroid) {
+ el.addEventListener('compositionstart', onCompositionStart);
+ el.addEventListener('compositionend', onCompositionEnd);
+ }
+ /* istanbul ignore if */
+ if (isIE9) {
+ el.vmodel = true;
+ }
+ }
+ },
+ componentUpdated: function componentUpdated (el, binding, vnode) {
+ if (vnode.tag === 'select') {
+ setSelected(el, binding, vnode.context);
+ // in case the options rendered by v-for have changed,
+ // it's possible that the value is out-of-sync with the rendered options.
+ // detect such cases and filter out values that no longer has a matchig
+ // option in the DOM.
+ var needReset = el.multiple
+ ? binding.value.some(function (v) { return hasNoMatchingOption(v, el.options); })
+ : binding.value !== binding.oldValue && hasNoMatchingOption(binding.value, el.options);
+ if (needReset) {
+ trigger(el, 'change');
+ }
+ }
+ }
+};
+
+function setSelected (el, binding, vm) {
+ var value = binding.value;
+ var isMultiple = el.multiple;
+ if (isMultiple && !Array.isArray(value)) {
+ "development" !== 'production' && warn(
+ "<select multiple v-model=\"" + (binding.expression) + "\"> " +
+ "expects an Array value for its binding, but got " + (Object.prototype.toString.call(value).slice(8, -1)),
+ vm
+ );
+ return
+ }
+ var selected, option;
+ for (var i = 0, l = el.options.length; i < l; i++) {
+ option = el.options[i];
+ if (isMultiple) {
+ selected = looseIndexOf(value, getValue(option)) > -1;
+ if (option.selected !== selected) {
+ option.selected = selected;
+ }
+ } else {
+ if (looseEqual(getValue(option), value)) {
+ if (el.selectedIndex !== i) {
+ el.selectedIndex = i;
}
+ return
}
}
+ }
+ if (!isMultiple) {
+ el.selectedIndex = -1;
+ }
+}
- if (termDef) {
- return makeTerminalNodeLinkFn(el, dirName, value, options, termDef, rawName, arg, modifiers);
+function hasNoMatchingOption (value, options) {
+ for (var i = 0, l = options.length; i < l; i++) {
+ if (looseEqual(getValue(options[i]), value)) {
+ return false
}
}
+ return true
+}
- function skip() {}
- skip.terminal = true;
+function getValue (option) {
+ return '_value' in option
+ ? option._value
+ : option.value
+}
- /**
- * Build a node link function for a terminal directive.
- * A terminal link function terminates the current
- * compilation recursion and handles compilation of the
- * subtree in the directive.
- *
- * @param {Element} el
- * @param {String} dirName
- * @param {String} value
- * @param {Object} options
- * @param {Object} def
- * @param {String} [rawName]
- * @param {String} [arg]
- * @param {Object} [modifiers]
- * @return {Function} terminalLinkFn
- */
+function onCompositionStart (e) {
+ e.target.composing = true;
+}
- function makeTerminalNodeLinkFn(el, dirName, value, options, def, rawName, arg, modifiers) {
- var parsed = parseDirective(value);
- var descriptor = {
- name: dirName,
- arg: arg,
- expression: parsed.expression,
- filters: parsed.filters,
- raw: value,
- attr: rawName,
- modifiers: modifiers,
- def: def
- };
- // check ref for v-for and router-view
- if (dirName === 'for' || dirName === 'router-view') {
- descriptor.ref = findRef(el);
- }
- var fn = function terminalNodeLinkFn(vm, el, host, scope, frag) {
- if (descriptor.ref) {
- defineReactive((scope || vm).$refs, descriptor.ref, null);
- }
- vm._bindDir(descriptor, el, host, scope, frag);
- };
- fn.terminal = true;
- return fn;
- }
+function onCompositionEnd (e) {
+ e.target.composing = false;
+ trigger(e.target, 'input');
+}
- /**
- * Compile the directives on an element and return a linker.
- *
- * @param {Array|NamedNodeMap} attrs
- * @param {Object} options
- * @return {Function}
- */
+function trigger (el, type) {
+ var e = document.createEvent('HTMLEvents');
+ e.initEvent(type, true, true);
+ el.dispatchEvent(e);
+}
- function compileDirectives(attrs, options) {
- var i = attrs.length;
- var dirs = [];
- var attr, name, value, rawName, rawValue, dirName, arg, modifiers, dirDef, tokens, matched;
- while (i--) {
- attr = attrs[i];
- name = rawName = attr.name;
- value = rawValue = attr.value;
- tokens = parseText(value);
- // reset arg
- arg = null;
- // check modifiers
- modifiers = parseModifiers(name);
- name = name.replace(modifierRE, '');
-
- // attribute interpolations
- if (tokens) {
- value = tokensToExp(tokens);
- arg = name;
- pushDir('bind', directives.bind, tokens);
- // warn against mixing mustaches with v-bind
- if ('development' !== 'production') {
- if (name === 'class' && Array.prototype.some.call(attrs, function (attr) {
- return attr.name === ':class' || attr.name === 'v-bind:class';
- })) {
- warn('class="' + rawValue + '": Do not mix mustache interpolation ' + 'and v-bind for "class" on the same element. Use one or the other.', options);
- }
- }
- } else
-
- // special attribute: transition
- if (transitionRE.test(name)) {
- modifiers.literal = !bindRE.test(name);
- pushDir('transition', internalDirectives.transition);
- } else
-
- // event handlers
- if (onRE.test(name)) {
- arg = name.replace(onRE, '');
- pushDir('on', directives.on);
- } else
-
- // attribute bindings
- if (bindRE.test(name)) {
- dirName = name.replace(bindRE, '');
- if (dirName === 'style' || dirName === 'class') {
- pushDir(dirName, internalDirectives[dirName]);
- } else {
- arg = dirName;
- pushDir('bind', directives.bind);
- }
- } else
-
- // normal directives
- if (matched = name.match(dirAttrRE)) {
- dirName = matched[1];
- arg = matched[2];
-
- // skip v-else (when used with v-show)
- if (dirName === 'else') {
- continue;
- }
-
- dirDef = resolveAsset(options, 'directives', dirName, true);
- if (dirDef) {
- pushDir(dirName, dirDef);
- }
- }
- }
+/* */
- /**
- * Push a directive.
- *
- * @param {String} dirName
- * @param {Object|Function} def
- * @param {Array} [interpTokens]
- */
-
- function pushDir(dirName, def, interpTokens) {
- var hasOneTimeToken = interpTokens && hasOneTime(interpTokens);
- var parsed = !hasOneTimeToken && parseDirective(value);
- dirs.push({
- name: dirName,
- attr: rawName,
- raw: rawValue,
- def: def,
- arg: arg,
- modifiers: modifiers,
- // conversion from interpolation strings with one-time token
- // to expression is differed until directive bind time so that we
- // have access to the actual vm context for one-time bindings.
- expression: parsed && parsed.expression,
- filters: parsed && parsed.filters,
- interp: interpTokens,
- hasOneTime: hasOneTimeToken
- });
- }
+// recursively search for possible transition defined inside the component root
+function locateNode (vnode) {
+ return vnode.child && (!vnode.data || !vnode.data.transition)
+ ? locateNode(vnode.child._vnode)
+ : vnode
+}
- if (dirs.length) {
- return makeNodeLinkFn(dirs);
- }
- }
+var show = {
+ bind: function bind (el, ref, vnode) {
+ var value = ref.value;
- /**
- * Parse modifiers from directive attribute name.
- *
- * @param {String} name
- * @return {Object}
- */
+ vnode = locateNode(vnode);
+ var transition = vnode.data && vnode.data.transition;
+ if (value && transition && !isIE9) {
+ enter(vnode);
+ }
+ var originalDisplay = el.style.display === 'none' ? '' : el.style.display;
+ el.style.display = value ? originalDisplay : 'none';
+ el.__vOriginalDisplay = originalDisplay;
+ },
+ update: function update (el, ref, vnode) {
+ var value = ref.value;
+ var oldValue = ref.oldValue;
- function parseModifiers(name) {
- var res = Object.create(null);
- var match = name.match(modifierRE);
- if (match) {
- var i = match.length;
- while (i--) {
- res[match[i].slice(1)] = true;
+ /* istanbul ignore if */
+ if (value === oldValue) { return }
+ vnode = locateNode(vnode);
+ var transition = vnode.data && vnode.data.transition;
+ if (transition && !isIE9) {
+ if (value) {
+ enter(vnode);
+ el.style.display = el.__vOriginalDisplay;
+ } else {
+ leave(vnode, function () {
+ el.style.display = 'none';
+ });
}
- }
- return res;
+ } else {
+ el.style.display = value ? el.__vOriginalDisplay : 'none';
+ }
+ }
+};
+
+var platformDirectives = {
+ model: model,
+ show: show
+};
+
+/* */
+
+// Provides transition support for a single element/component.
+// supports transition mode (out-in / in-out)
+
+var transitionProps = {
+ name: String,
+ appear: Boolean,
+ css: Boolean,
+ mode: String,
+ type: String,
+ enterClass: String,
+ leaveClass: String,
+ enterActiveClass: String,
+ leaveActiveClass: String,
+ appearClass: String,
+ appearActiveClass: String
+};
+
+// in case the child is also an abstract component, e.g. <keep-alive>
+// we want to recrusively retrieve the real component to be rendered
+function getRealChild (vnode) {
+ var compOptions = vnode && vnode.componentOptions;
+ if (compOptions && compOptions.Ctor.options.abstract) {
+ return getRealChild(getFirstComponentChild(compOptions.children))
+ } else {
+ return vnode
}
+}
- /**
- * Build a link function for all directives on a single node.
- *
- * @param {Array} directives
- * @return {Function} directivesLinkFn
- */
-
- function makeNodeLinkFn(directives) {
- return function nodeLinkFn(vm, el, host, scope, frag) {
- // reverse apply because it's sorted low to high
- var i = directives.length;
- while (i--) {
- vm._bindDir(directives[i], el, host, scope, frag);
- }
- };
+function extractTransitionData (comp) {
+ var data = {};
+ var options = comp.$options;
+ // props
+ for (var key in options.propsData) {
+ data[key] = comp[key];
+ }
+ // events.
+ // extract listeners and pass them directly to the transition methods
+ var listeners = options._parentListeners;
+ for (var key$1 in listeners) {
+ data[camelize(key$1)] = listeners[key$1].fn;
}
+ return data
+}
- /**
- * Check if an interpolation string contains one-time tokens.
- *
- * @param {Array} tokens
- * @return {Boolean}
- */
+function placeholder (h, rawChild) {
+ return /\d-keep-alive$/.test(rawChild.tag)
+ ? h('keep-alive')
+ : null
+}
- function hasOneTime(tokens) {
- var i = tokens.length;
- while (i--) {
- if (tokens[i].oneTime) return true;
+function hasParentTransition (vnode) {
+ while ((vnode = vnode.parent)) {
+ if (vnode.data.transition) {
+ return true
}
}
+}
- function isScript(el) {
- return el.tagName === 'SCRIPT' && (!el.hasAttribute('type') || el.getAttribute('type') === 'text/javascript');
- }
-
- var specialCharRE = /[^\w\-:\.]/;
+var Transition = {
+ name: 'transition',
+ props: transitionProps,
+ abstract: true,
+ render: function render (h) {
+ var this$1 = this;
- /**
- * Process an element or a DocumentFragment based on a
- * instance option object. This allows us to transclude
- * a template node/fragment before the instance is created,
- * so the processed fragment can then be cloned and reused
- * in v-for.
- *
- * @param {Element} el
- * @param {Object} options
- * @return {Element|DocumentFragment}
- */
-
- function transclude(el, options) {
- // extract container attributes to pass them down
- // to compiler, because they need to be compiled in
- // parent scope. we are mutating the options object here
- // assuming the same object will be used for compile
- // right after this.
- if (options) {
- options._containerAttrs = extractAttrs(el);
+ var children = this.$slots.default;
+ if (!children) {
+ return
}
- // for template tags, what we want is its content as
- // a documentFragment (for fragment instances)
- if (isTemplate(el)) {
- el = parseTemplate(el);
- }
- if (options) {
- if (options._asComponent && !options.template) {
- options.template = '<slot></slot>';
- }
- if (options.template) {
- options._content = extractContent(el);
- el = transcludeTemplate(el, options);
- }
+
+ // filter out text nodes (possible whitespaces)
+ children = children.filter(function (c) { return c.tag; });
+ /* istanbul ignore if */
+ if (!children.length) {
+ return
}
- if (isFragment(el)) {
- // anchors for fragment instance
- // passing in `persist: true` to avoid them being
- // discarded by IE during template cloning
- prepend(createAnchor('v-start', true), el);
- el.appendChild(createAnchor('v-end', true));
+
+ // warn multiple elements
+ if ("development" !== 'production' && children.length > 1) {
+ warn(
+ '<transition> can only be used on a single element. Use ' +
+ '<transition-group> for lists.',
+ this.$parent
+ );
}
- return el;
- }
- /**
- * Process the template option.
- * If the replace option is true this will swap the $el.
- *
- * @param {Element} el
- * @param {Object} options
- * @return {Element|DocumentFragment}
- */
+ var mode = this.mode;
- function transcludeTemplate(el, options) {
- var template = options.template;
- var frag = parseTemplate(template, true);
- if (frag) {
- var replacer = frag.firstChild;
- var tag = replacer.tagName && replacer.tagName.toLowerCase();
- if (options.replace) {
- /* istanbul ignore if */
- if (el === document.body) {
- 'development' !== 'production' && warn('You are mounting an instance with a template to ' + '<body>. This will replace <body> entirely. You ' + 'should probably use `replace: false` here.');
- }
- // there are many cases where the instance must
- // become a fragment instance: basically anything that
- // can create more than 1 root nodes.
- if (
- // multi-children template
- frag.childNodes.length > 1 ||
- // non-element template
- replacer.nodeType !== 1 ||
- // single nested component
- tag === 'component' || resolveAsset(options, 'components', tag) || hasBindAttr(replacer, 'is') ||
- // element directive
- resolveAsset(options, 'elementDirectives', tag) ||
- // for block
- replacer.hasAttribute('v-for') ||
- // if block
- replacer.hasAttribute('v-if')) {
- return frag;
- } else {
- options._replacerAttrs = extractAttrs(replacer);
- mergeAttrs(el, replacer);
- return replacer;
- }
- } else {
- el.appendChild(frag);
- return el;
- }
- } else {
- 'development' !== 'production' && warn('Invalid template option: ' + template);
+ // warn invalid mode
+ if ("development" !== 'production' &&
+ mode && mode !== 'in-out' && mode !== 'out-in') {
+ warn(
+ 'invalid <transition> mode: ' + mode,
+ this.$parent
+ );
}
- }
- /**
- * Helper to extract a component container's attributes
- * into a plain object array.
- *
- * @param {Element} el
- * @return {Array}
- */
+ var rawChild = children[0];
- function extractAttrs(el) {
- if (el.nodeType === 1 && el.hasAttributes()) {
- return toArray(el.attributes);
+ // if this is a component root node and the component's
+ // parent container node also has transition, skip.
+ if (hasParentTransition(this.$vnode)) {
+ return rawChild
}
- }
- /**
- * Merge the attributes of two elements, and make sure
- * the class names are merged properly.
- *
- * @param {Element} from
- * @param {Element} to
- */
-
- function mergeAttrs(from, to) {
- var attrs = from.attributes;
- var i = attrs.length;
- var name, value;
- while (i--) {
- name = attrs[i].name;
- value = attrs[i].value;
- if (!to.hasAttribute(name) && !specialCharRE.test(name)) {
- to.setAttribute(name, value);
- } else if (name === 'class' && !parseText(value) && (value = value.trim())) {
- value.split(/\s+/).forEach(function (cls) {
- addClass(to, cls);
- });
+ // apply transition data to child
+ // use getRealChild() to ignore abstract components e.g. keep-alive
+ var child = getRealChild(rawChild);
+ /* istanbul ignore if */
+ if (!child) {
+ return rawChild
+ }
+
+ if (this._leaving) {
+ return placeholder(h, rawChild)
+ }
+
+ var key = child.key = child.key == null || child.isStatic
+ ? ("__v" + (child.tag + this._uid) + "__")
+ : child.key;
+ var data = (child.data || (child.data = {})).transition = extractTransitionData(this);
+ var oldRawChild = this._vnode;
+ var oldChild = getRealChild(oldRawChild);
+
+ // mark v-show
+ // so that the transition module can hand over the control to the directive
+ if (child.data.directives && child.data.directives.some(function (d) { return d.name === 'show'; })) {
+ child.data.show = true;
+ }
+
+ if (oldChild && oldChild.data && oldChild.key !== key) {
+ // replace old child transition data with fresh one
+ // important for dynamic transitions!
+ var oldData = oldChild.data.transition = extend({}, data);
+
+ // handle transition mode
+ if (mode === 'out-in') {
+ // return placeholder node and queue update when leave finishes
+ this._leaving = true;
+ mergeVNodeHook(oldData, 'afterLeave', function () {
+ this$1._leaving = false;
+ this$1.$forceUpdate();
+ }, key);
+ return placeholder(h, rawChild)
+ } else if (mode === 'in-out') {
+ var delayedLeave;
+ var performLeave = function () { delayedLeave(); };
+ mergeVNodeHook(data, 'afterEnter', performLeave, key);
+ mergeVNodeHook(data, 'enterCancelled', performLeave, key);
+ mergeVNodeHook(oldData, 'delayLeave', function (leave) {
+ delayedLeave = leave;
+ }, key);
+ }
+ }
+
+ return rawChild
+ }
+};
+
+/* */
+
+// Provides transition support for list items.
+// supports move transitions using the FLIP technique.
+
+// Because the vdom's children update algorithm is "unstable" - i.e.
+// it doesn't guarantee the relative positioning of removed elements,
+// we force transition-group to update its children into two passes:
+// in the first pass, we remove all nodes that need to be removed,
+// triggering their leaving transition; in the second pass, we insert/move
+// into the final disired state. This way in the second pass removed
+// nodes will remain where they should be.
+
+var props = extend({
+ tag: String,
+ moveClass: String
+}, transitionProps);
+
+delete props.mode;
+
+var TransitionGroup = {
+ props: props,
+
+ render: function render (h) {
+ var tag = this.tag || this.$vnode.data.tag || 'span';
+ var map = Object.create(null);
+ var prevChildren = this.prevChildren = this.children;
+ var rawChildren = this.$slots.default || [];
+ var children = this.children = [];
+ var transitionData = extractTransitionData(this);
+
+ for (var i = 0; i < rawChildren.length; i++) {
+ var c = rawChildren[i];
+ if (c.tag) {
+ if (c.key != null && String(c.key).indexOf('__vlist') !== 0) {
+ children.push(c);
+ map[c.key] = c
+ ;(c.data || (c.data = {})).transition = transitionData;
+ } else {
+ var opts = c.componentOptions;
+ var name = opts
+ ? (opts.Ctor.options.name || opts.tag)
+ : c.tag;
+ warn(("<transition-group> children must be keyed: <" + name + ">"));
+ }
}
}
- }
-
- /**
- * Scan and determine slot content distribution.
- * We do this during transclusion instead at compile time so that
- * the distribution is decoupled from the compilation order of
- * the slots.
- *
- * @param {Element|DocumentFragment} template
- * @param {Element} content
- * @param {Vue} vm
- */
- function resolveSlots(vm, content) {
- if (!content) {
- return;
- }
- var contents = vm._slotContents = Object.create(null);
- var el, name;
- for (var i = 0, l = content.children.length; i < l; i++) {
- el = content.children[i];
- /* eslint-disable no-cond-assign */
- if (name = el.getAttribute('slot')) {
- (contents[name] || (contents[name] = [])).push(el);
+ if (prevChildren) {
+ var kept = [];
+ var removed = [];
+ for (var i$1 = 0; i$1 < prevChildren.length; i$1++) {
+ var c$1 = prevChildren[i$1];
+ c$1.data.transition = transitionData;
+ c$1.data.pos = c$1.elm.getBoundingClientRect();
+ if (map[c$1.key]) {
+ kept.push(c$1);
+ } else {
+ removed.push(c$1);
+ }
+ }
+ this.kept = h(tag, null, kept);
+ this.removed = removed;
+ }
+
+ return h(tag, null, children)
+ },
+
+ beforeUpdate: function beforeUpdate () {
+ // force removing pass
+ this.__patch__(
+ this._vnode,
+ this.kept,
+ false, // hydrating
+ true // removeOnly (!important, avoids unnecessary moves)
+ );
+ this._vnode = this.kept;
+ },
+
+ updated: function updated () {
+ var children = this.prevChildren;
+ var moveClass = this.moveClass || (this.name + '-move');
+ if (!children.length || !this.hasMove(children[0].elm, moveClass)) {
+ return
+ }
+
+ // we divide the work into three loops to avoid mixing DOM reads and writes
+ // in each iteration - which helps prevent layout thrashing.
+ children.forEach(callPendingCbs);
+ children.forEach(recordPosition);
+ children.forEach(applyTranslation);
+
+ // force reflow to put everything in position
+ var f = document.body.offsetHeight; // eslint-disable-line
+
+ children.forEach(function (c) {
+ if (c.data.moved) {
+ var el = c.elm;
+ var s = el.style;
+ addTransitionClass(el, moveClass);
+ s.transform = s.WebkitTransform = s.transitionDuration = '';
+ el.addEventListener(transitionEndEvent, el._moveCb = function cb (e) {
+ if (!e || /transform$/.test(e.propertyName)) {
+ el.removeEventListener(transitionEndEvent, cb);
+ el._moveCb = null;
+ removeTransitionClass(el, moveClass);
+ }
+ });
}
- /* eslint-enable no-cond-assign */
- if ('development' !== 'production' && getBindAttr(el, 'slot')) {
- warn('The "slot" attribute must be static.', vm.$parent);
+ });
+ },
+
+ methods: {
+ hasMove: function hasMove (el, moveClass) {
+ /* istanbul ignore if */
+ if (!hasTransition) {
+ return false
}
- }
- for (name in contents) {
- contents[name] = extractFragment(contents[name], content);
- }
- if (content.hasChildNodes()) {
- var nodes = content.childNodes;
- if (nodes.length === 1 && nodes[0].nodeType === 3 && !nodes[0].data.trim()) {
- return;
+ if (this._hasMove != null) {
+ return this._hasMove
}
- contents['default'] = extractFragment(content.childNodes, content);
+ addTransitionClass(el, moveClass);
+ var info = getTransitionInfo(el);
+ removeTransitionClass(el, moveClass);
+ return (this._hasMove = info.hasTransform)
}
}
+};
- /**
- * Extract qualified content nodes from a node list.
- *
- * @param {NodeList} nodes
- * @return {DocumentFragment}
- */
-
- function extractFragment(nodes, parent) {
- var frag = document.createDocumentFragment();
- nodes = toArray(nodes);
- for (var i = 0, l = nodes.length; i < l; i++) {
- var node = nodes[i];
- if (isTemplate(node) && !node.hasAttribute('v-if') && !node.hasAttribute('v-for')) {
- parent.removeChild(node);
- node = parseTemplate(node, true);
- }
- frag.appendChild(node);
+function callPendingCbs (c) {
+ /* istanbul ignore if */
+ if (c.elm._moveCb) {
+ c.elm._moveCb();
+ }
+ /* istanbul ignore if */
+ if (c.elm._enterCb) {
+ c.elm._enterCb();
+ }
+}
+
+function recordPosition (c) {
+ c.data.newPos = c.elm.getBoundingClientRect();
+}
+
+function applyTranslation (c) {
+ var oldPos = c.data.pos;
+ var newPos = c.data.newPos;
+ var dx = oldPos.left - newPos.left;
+ var dy = oldPos.top - newPos.top;
+ if (dx || dy) {
+ c.data.moved = true;
+ var s = c.elm.style;
+ s.transform = s.WebkitTransform = "translate(" + dx + "px," + dy + "px)";
+ s.transitionDuration = '0s';
+ }
+}
+
+var platformComponents = {
+ Transition: Transition,
+ TransitionGroup: TransitionGroup
+};
+
+/* */
+
+// install platform specific utils
+Vue$3.config.isUnknownElement = isUnknownElement;
+Vue$3.config.isReservedTag = isReservedTag;
+Vue$3.config.getTagNamespace = getTagNamespace;
+Vue$3.config.mustUseProp = mustUseProp;
+
+// install platform runtime directives & components
+extend(Vue$3.options.directives, platformDirectives);
+extend(Vue$3.options.components, platformComponents);
+
+// install platform patch function
+Vue$3.prototype.__patch__ = config._isServer ? noop : patch$1;
+
+// wrap mount
+Vue$3.prototype.$mount = function (
+ el,
+ hydrating
+) {
+ el = el && !config._isServer ? query(el) : undefined;
+ return this._mount(el, hydrating)
+};
+
+// devtools global hook
+/* istanbul ignore next */
+setTimeout(function () {
+ if (config.devtools) {
+ if (devtools) {
+ devtools.emit('init', Vue$3);
+ } else if (
+ "development" !== 'production' &&
+ inBrowser && /Chrome\/\d+/.test(window.navigator.userAgent)
+ ) {
+ console.log(
+ 'Download the Vue Devtools for a better development experience:\n' +
+ 'https://github.com/vuejs/vue-devtools'
+ );
}
- return frag;
}
+}, 0);
+/* */
+// check whether current browser encodes a char inside attribute values
+function shouldDecode (content, encoded) {
+ var div = document.createElement('div');
+ div.innerHTML = "<div a=\"" + content + "\">";
+ return div.innerHTML.indexOf(encoded) > 0
+}
- var compiler = Object.freeze({
- compile: compile,
- compileAndLinkProps: compileAndLinkProps,
- compileRoot: compileRoot,
- transclude: transclude,
- resolveSlots: resolveSlots
- });
+// #3663
+// IE encodes newlines inside attribute values while other browsers don't
+var shouldDecodeNewlines = inBrowser ? shouldDecode('\n', '&#10;') : false;
- function stateMixin (Vue) {
- /**
- * Accessor for `$data` property, since setting $data
- * requires observing the new object and updating
- * proxied properties.
- */
-
- Object.defineProperty(Vue.prototype, '$data', {
- get: function get() {
- return this._data;
- },
- set: function set(newData) {
- if (newData !== this._data) {
- this._setData(newData);
- }
- }
- });
+/* */
- /**
- * Setup the scope of an instance, which contains:
- * - observed data
- * - computed properties
- * - user methods
- * - meta properties
- */
-
- Vue.prototype._initState = function () {
- this._initProps();
- this._initMeta();
- this._initMethods();
- this._initData();
- this._initComputed();
- };
+var decoder = document.createElement('div');
- /**
- * Initialize props.
- */
-
- Vue.prototype._initProps = function () {
- var options = this.$options;
- var el = options.el;
- var props = options.props;
- if (props && !el) {
- 'development' !== 'production' && warn('Props will not be compiled if no `el` option is ' + 'provided at instantiation.', this);
- }
- // make sure to convert string selectors into element now
- el = options.el = query(el);
- this._propsUnlinkFn = el && el.nodeType === 1 && props
- // props must be linked in proper scope if inside v-for
- ? compileAndLinkProps(this, el, props, this._scope) : null;
- };
+function decode (html) {
+ decoder.innerHTML = html;
+ return decoder.textContent
+}
- /**
- * Initialize the data.
- */
-
- Vue.prototype._initData = function () {
- var dataFn = this.$options.data;
- var data = this._data = dataFn ? dataFn() : {};
- if (!isPlainObject(data)) {
- data = {};
- 'development' !== 'production' && warn('data functions should return an object.', this);
- }
- var props = this._props;
- // proxy data on instance
- var keys = Object.keys(data);
- var i, key;
- i = keys.length;
- while (i--) {
- key = keys[i];
- // there are two scenarios where we can proxy a data key:
- // 1. it's not already defined as a prop
- // 2. it's provided via a instantiation option AND there are no
- // template prop present
- if (!props || !hasOwn(props, key)) {
- this._proxy(key);
- } else if ('development' !== 'production') {
- warn('Data field "' + key + '" is already defined ' + 'as a prop. To provide default value for a prop, use the "default" ' + 'prop option; if you want to pass prop values to an instantiation ' + 'call, use the "propsData" option.', this);
- }
- }
- // observe data
- observe(data, this);
- };
+/**
+ * Not type-checking this file because it's mostly vendor code.
+ */
- /**
- * Swap the instance's $data. Called in $data's setter.
- *
- * @param {Object} newData
- */
-
- Vue.prototype._setData = function (newData) {
- newData = newData || {};
- var oldData = this._data;
- this._data = newData;
- var keys, key, i;
- // unproxy keys not present in new data
- keys = Object.keys(oldData);
- i = keys.length;
- while (i--) {
- key = keys[i];
- if (!(key in newData)) {
- this._unproxy(key);
- }
- }
- // proxy keys not already proxied,
- // and trigger change for changed values
- keys = Object.keys(newData);
- i = keys.length;
- while (i--) {
- key = keys[i];
- if (!hasOwn(this, key)) {
- // new property
- this._proxy(key);
- }
- }
- oldData.__ob__.removeVm(this);
- observe(newData, this);
- this._digest();
- };
+/*!
+ * HTML Parser By John Resig (ejohn.org)
+ * Modified by Juriy "kangax" Zaytsev
+ * Original code by Erik Arvidsson, Mozilla Public License
+ * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
+ */
- /**
- * Proxy a property, so that
- * vm.prop === vm._data.prop
- *
- * @param {String} key
- */
-
- Vue.prototype._proxy = function (key) {
- if (!isReserved(key)) {
- // need to store ref to self here
- // because these getter/setters might
- // be called by child scopes via
- // prototype inheritance.
- var self = this;
- Object.defineProperty(self, key, {
- configurable: true,
- enumerable: true,
- get: function proxyGetter() {
- return self._data[key];
- },
- set: function proxySetter(val) {
- self._data[key] = val;
+// Regular Expressions for parsing tags and attributes
+var singleAttrIdentifier = /([^\s"'<>\/=]+)/;
+var singleAttrAssign = /(?:=)/;
+var singleAttrValues = [
+ // attr value double quotes
+ /"([^"]*)"+/.source,
+ // attr value, single quotes
+ /'([^']*)'+/.source,
+ // attr value, no quotes
+ /([^\s"'=<>`]+)/.source
+];
+var attribute = new RegExp(
+ '^\\s*' + singleAttrIdentifier.source +
+ '(?:\\s*(' + singleAttrAssign.source + ')' +
+ '\\s*(?:' + singleAttrValues.join('|') + '))?'
+);
+
+// could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
+// but for Vue templates we can enforce a simple charset
+var ncname = '[a-zA-Z_][\\w\\-\\.]*';
+var qnameCapture = '((?:' + ncname + '\\:)?' + ncname + ')';
+var startTagOpen = new RegExp('^<' + qnameCapture);
+var startTagClose = /^\s*(\/?)>/;
+var endTag = new RegExp('^<\\/' + qnameCapture + '[^>]*>');
+var doctype = /^<!DOCTYPE [^>]+>/i;
+
+var IS_REGEX_CAPTURING_BROKEN = false;
+'x'.replace(/x(.)?/g, function (m, g) {
+ IS_REGEX_CAPTURING_BROKEN = g === '';
+});
+
+// Special Elements (can contain anything)
+var isSpecialTag = makeMap('script,style', true);
+
+var reCache = {};
+
+var ltRE = /&lt;/g;
+var gtRE = /&gt;/g;
+var nlRE = /&#10;/g;
+var ampRE = /&amp;/g;
+var quoteRE = /&quot;/g;
+
+function decodeAttr (value, shouldDecodeNewlines) {
+ if (shouldDecodeNewlines) {
+ value = value.replace(nlRE, '\n');
+ }
+ return value
+ .replace(ltRE, '<')
+ .replace(gtRE, '>')
+ .replace(ampRE, '&')
+ .replace(quoteRE, '"')
+}
+
+function parseHTML (html, options) {
+ var stack = [];
+ var expectHTML = options.expectHTML;
+ var isUnaryTag$$1 = options.isUnaryTag || no;
+ var index = 0;
+ var last, lastTag;
+ while (html) {
+ last = html;
+ // Make sure we're not in a script or style element
+ if (!lastTag || !isSpecialTag(lastTag)) {
+ var textEnd = html.indexOf('<');
+ if (textEnd === 0) {
+ // Comment:
+ if (/^<!--/.test(html)) {
+ var commentEnd = html.indexOf('-->');
+
+ if (commentEnd >= 0) {
+ advance(commentEnd + 3);
+ continue
}
- });
- }
- };
-
- /**
- * Unproxy a property.
- *
- * @param {String} key
- */
-
- Vue.prototype._unproxy = function (key) {
- if (!isReserved(key)) {
- delete this[key];
- }
- };
-
- /**
- * Force update on every watcher in scope.
- */
+ }
- Vue.prototype._digest = function () {
- for (var i = 0, l = this._watchers.length; i < l; i++) {
- this._watchers[i].update(true); // shallow updates
- }
- };
+ // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
+ if (/^<!\[/.test(html)) {
+ var conditionalEnd = html.indexOf(']>');
- /**
- * Setup computed properties. They are essentially
- * special getter/setters
- */
-
- function noop() {}
- Vue.prototype._initComputed = function () {
- var computed = this.$options.computed;
- if (computed) {
- for (var key in computed) {
- var userDef = computed[key];
- var def = {
- enumerable: true,
- configurable: true
- };
- if (typeof userDef === 'function') {
- def.get = makeComputedGetter(userDef, this);
- def.set = noop;
- } else {
- def.get = userDef.get ? userDef.cache !== false ? makeComputedGetter(userDef.get, this) : bind(userDef.get, this) : noop;
- def.set = userDef.set ? bind(userDef.set, this) : noop;
+ if (conditionalEnd >= 0) {
+ advance(conditionalEnd + 2);
+ continue
}
- Object.defineProperty(this, key, def);
}
- }
- };
- function makeComputedGetter(getter, owner) {
- var watcher = new Watcher(owner, getter, null, {
- lazy: true
- });
- return function computedGetter() {
- if (watcher.dirty) {
- watcher.evaluate();
+ // Doctype:
+ var doctypeMatch = html.match(doctype);
+ if (doctypeMatch) {
+ advance(doctypeMatch[0].length);
+ continue
}
- if (Dep.target) {
- watcher.depend();
- }
- return watcher.value;
- };
- }
- /**
- * Setup instance methods. Methods must be bound to the
- * instance since they might be passed down as a prop to
- * child components.
- */
-
- Vue.prototype._initMethods = function () {
- var methods = this.$options.methods;
- if (methods) {
- for (var key in methods) {
- this[key] = bind(methods[key], this);
+ // End tag:
+ var endTagMatch = html.match(endTag);
+ if (endTagMatch) {
+ var curIndex = index;
+ advance(endTagMatch[0].length);
+ parseEndTag(endTagMatch[0], endTagMatch[1], curIndex, index);
+ continue
}
- }
- };
-
- /**
- * Initialize meta information like $index, $key & $value.
- */
- Vue.prototype._initMeta = function () {
- var metas = this.$options._meta;
- if (metas) {
- for (var key in metas) {
- defineReactive(this, key, metas[key]);
+ // Start tag:
+ var startTagMatch = parseStartTag();
+ if (startTagMatch) {
+ handleStartTag(startTagMatch);
+ continue
}
}
- };
- }
-
- var eventRE = /^v-on:|^@/;
-
- function eventsMixin (Vue) {
- /**
- * Setup the instance's option events & watchers.
- * If the value is a string, we pull it from the
- * instance's methods by name.
- */
-
- Vue.prototype._initEvents = function () {
- var options = this.$options;
- if (options._asComponent) {
- registerComponentEvents(this, options.el);
- }
- registerCallbacks(this, '$on', options.events);
- registerCallbacks(this, '$watch', options.watch);
- };
- /**
- * Register v-on events on a child component
- *
- * @param {Vue} vm
- * @param {Element} el
- */
-
- function registerComponentEvents(vm, el) {
- var attrs = el.attributes;
- var name, value, handler;
- for (var i = 0, l = attrs.length; i < l; i++) {
- name = attrs[i].name;
- if (eventRE.test(name)) {
- name = name.replace(eventRE, '');
- // force the expression into a statement so that
- // it always dynamically resolves the method to call (#2670)
- // kinda ugly hack, but does the job.
- value = attrs[i].value;
- if (isSimplePath(value)) {
- value += '.apply(this, $arguments)';
- }
- handler = (vm._scope || vm._context).$eval(value, true);
- handler._fromParent = true;
- vm.$on(name.replace(eventRE), handler);
- }
+ var text = void 0;
+ if (textEnd >= 0) {
+ text = html.substring(0, textEnd);
+ advance(textEnd);
+ } else {
+ text = html;
+ html = '';
}
- }
- /**
- * Register callbacks for option events and watchers.
- *
- * @param {Vue} vm
- * @param {String} action
- * @param {Object} hash
- */
-
- function registerCallbacks(vm, action, hash) {
- if (!hash) return;
- var handlers, key, i, j;
- for (key in hash) {
- handlers = hash[key];
- if (isArray(handlers)) {
- for (i = 0, j = handlers.length; i < j; i++) {
- register(vm, action, key, handlers[i]);
- }
- } else {
- register(vm, action, key, handlers);
- }
+ if (options.chars) {
+ options.chars(text);
}
+ } else {
+ var stackedTag = lastTag.toLowerCase();
+ var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'));
+ var endTagLength = 0;
+ var rest = html.replace(reStackedTag, function (all, text, endTag) {
+ endTagLength = endTag.length;
+ if (stackedTag !== 'script' && stackedTag !== 'style' && stackedTag !== 'noscript') {
+ text = text
+ .replace(/<!--([\s\S]*?)-->/g, '$1')
+ .replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1');
+ }
+ if (options.chars) {
+ options.chars(text);
+ }
+ return ''
+ });
+ index += html.length - rest.length;
+ html = rest;
+ parseEndTag('</' + stackedTag + '>', stackedTag, index - endTagLength, index);
}
- /**
- * Helper to register an event/watch callback.
- *
- * @param {Vue} vm
- * @param {String} action
- * @param {String} key
- * @param {Function|String|Object} handler
- * @param {Object} [options]
- */
-
- function register(vm, action, key, handler, options) {
- var type = typeof handler;
- if (type === 'function') {
- vm[action](key, handler, options);
- } else if (type === 'string') {
- var methods = vm.$options.methods;
- var method = methods && methods[handler];
- if (method) {
- vm[action](key, method, options);
- } else {
- 'development' !== 'production' && warn('Unknown method: "' + handler + '" when ' + 'registering callback for ' + action + ': "' + key + '".', vm);
- }
- } else if (handler && type === 'object') {
- register(vm, action, key, handler.handler, handler);
- }
+ if (html === last) {
+ throw new Error('Error parsing template:\n\n' + html)
}
+ }
- /**
- * Setup recursive attached/detached calls
- */
-
- Vue.prototype._initDOMHooks = function () {
- this.$on('hook:attached', onAttached);
- this.$on('hook:detached', onDetached);
- };
+ // Clean up any remaining tags
+ parseEndTag();
- /**
- * Callback to recursively call attached hook on children
- */
+ function advance (n) {
+ index += n;
+ html = html.substring(n);
+ }
- function onAttached() {
- if (!this._isAttached) {
- this._isAttached = true;
- this.$children.forEach(callAttach);
+ function parseStartTag () {
+ var start = html.match(startTagOpen);
+ if (start) {
+ var match = {
+ tagName: start[1],
+ attrs: [],
+ start: index
+ };
+ advance(start[0].length);
+ var end, attr;
+ while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
+ advance(attr[0].length);
+ match.attrs.push(attr);
}
- }
-
- /**
- * Iterator to call attached hook
- *
- * @param {Vue} child
- */
-
- function callAttach(child) {
- if (!child._isAttached && inDoc(child.$el)) {
- child._callHook('attached');
+ if (end) {
+ match.unarySlash = end[1];
+ advance(end[0].length);
+ match.end = index;
+ return match
}
}
+ }
- /**
- * Callback to recursively call detached hook on children
- */
+ function handleStartTag (match) {
+ var tagName = match.tagName;
+ var unarySlash = match.unarySlash;
- function onDetached() {
- if (this._isAttached) {
- this._isAttached = false;
- this.$children.forEach(callDetach);
+ if (expectHTML) {
+ if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
+ parseEndTag('', lastTag);
}
- }
-
- /**
- * Iterator to call detached hook
- *
- * @param {Vue} child
- */
-
- function callDetach(child) {
- if (child._isAttached && !inDoc(child.$el)) {
- child._callHook('detached');
+ if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
+ parseEndTag('', tagName);
}
}
- /**
- * Trigger all handlers for a hook
- *
- * @param {String} hook
- */
+ var unary = isUnaryTag$$1(tagName) || tagName === 'html' && lastTag === 'head' || !!unarySlash;
- Vue.prototype._callHook = function (hook) {
- this.$emit('pre-hook:' + hook);
- var handlers = this.$options[hook];
- if (handlers) {
- for (var i = 0, j = handlers.length; i < j; i++) {
- handlers[i].call(this);
- }
+ var l = match.attrs.length;
+ var attrs = new Array(l);
+ for (var i = 0; i < l; i++) {
+ var args = match.attrs[i];
+ // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
+ if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
+ if (args[3] === '') { delete args[3]; }
+ if (args[4] === '') { delete args[4]; }
+ if (args[5] === '') { delete args[5]; }
}
- this.$emit('hook:' + hook);
- };
- }
+ var value = args[3] || args[4] || args[5] || '';
+ attrs[i] = {
+ name: args[1],
+ value: decodeAttr(
+ value,
+ options.shouldDecodeNewlines
+ )
+ };
+ }
- function noop$1() {}
+ if (!unary) {
+ stack.push({ tag: tagName, attrs: attrs });
+ lastTag = tagName;
+ unarySlash = '';
+ }
- /**
- * A directive links a DOM element with a piece of data,
- * which is the result of evaluating an expression.
- * It registers a watcher with the expression and calls
- * the DOM update function when a change is triggered.
- *
- * @param {Object} descriptor
- * - {String} name
- * - {Object} def
- * - {String} expression
- * - {Array<Object>} [filters]
- * - {Object} [modifiers]
- * - {Boolean} literal
- * - {String} attr
- * - {String} arg
- * - {String} raw
- * - {String} [ref]
- * - {Array<Object>} [interp]
- * - {Boolean} [hasOneTime]
- * @param {Vue} vm
- * @param {Node} el
- * @param {Vue} [host] - transclusion host component
- * @param {Object} [scope] - v-for scope
- * @param {Fragment} [frag] - owner fragment
- * @constructor
- */
- function Directive(descriptor, vm, el, host, scope, frag) {
- this.vm = vm;
- this.el = el;
- // copy descriptor properties
- this.descriptor = descriptor;
- this.name = descriptor.name;
- this.expression = descriptor.expression;
- this.arg = descriptor.arg;
- this.modifiers = descriptor.modifiers;
- this.filters = descriptor.filters;
- this.literal = this.modifiers && this.modifiers.literal;
- // private
- this._locked = false;
- this._bound = false;
- this._listeners = null;
- // link context
- this._host = host;
- this._scope = scope;
- this._frag = frag;
- // store directives on node in dev mode
- if ('development' !== 'production' && this.el) {
- this.el._vue_directives = this.el._vue_directives || [];
- this.el._vue_directives.push(this);
+ if (options.start) {
+ options.start(tagName, attrs, unary, match.start, match.end);
}
}
- /**
- * Initialize the directive, mixin definition properties,
- * setup the watcher, call definition bind() and update()
- * if present.
- */
-
- Directive.prototype._bind = function () {
- var name = this.name;
- var descriptor = this.descriptor;
+ function parseEndTag (tag, tagName, start, end) {
+ var pos;
+ if (start == null) { start = index; }
+ if (end == null) { end = index; }
- // remove attribute
- if ((name !== 'cloak' || this.vm._isCompiled) && this.el && this.el.removeAttribute) {
- var attr = descriptor.attr || 'v-' + name;
- this.el.removeAttribute(attr);
- }
-
- // copy def properties
- var def = descriptor.def;
- if (typeof def === 'function') {
- this.update = def;
+ // Find the closest opened tag of the same type
+ if (tagName) {
+ var needle = tagName.toLowerCase();
+ for (pos = stack.length - 1; pos >= 0; pos--) {
+ if (stack[pos].tag.toLowerCase() === needle) {
+ break
+ }
+ }
} else {
- extend(this, def);
+ // If no tag name is provided, clean shop
+ pos = 0;
}
- // setup directive params
- this._setupParams();
-
- // initial bind
- if (this.bind) {
- this.bind();
- }
- this._bound = true;
+ if (pos >= 0) {
+ // Close all the open elements, up the stack
+ for (var i = stack.length - 1; i >= pos; i--) {
+ if (options.end) {
+ options.end(stack[i].tag, start, end);
+ }
+ }
- if (this.literal) {
- this.update && this.update(descriptor.raw);
- } else if ((this.expression || this.modifiers) && (this.update || this.twoWay) && !this._checkStatement()) {
- // wrapped updater for context
- var dir = this;
- if (this.update) {
- this._update = function (val, oldVal) {
- if (!dir._locked) {
- dir.update(val, oldVal);
- }
- };
- } else {
- this._update = noop$1;
+ // Remove the open elements from the stack
+ stack.length = pos;
+ lastTag = pos && stack[pos - 1].tag;
+ } else if (tagName.toLowerCase() === 'br') {
+ if (options.start) {
+ options.start(tagName, [], true, start, end);
}
- var preProcess = this._preProcess ? bind(this._preProcess, this) : null;
- var postProcess = this._postProcess ? bind(this._postProcess, this) : null;
- var watcher = this._watcher = new Watcher(this.vm, this.expression, this._update, // callback
- {
- filters: this.filters,
- twoWay: this.twoWay,
- deep: this.deep,
- preProcess: preProcess,
- postProcess: postProcess,
- scope: this._scope
- });
- // v-model with inital inline value need to sync back to
- // model instead of update to DOM on init. They would
- // set the afterBind hook to indicate that.
- if (this.afterBind) {
- this.afterBind();
- } else if (this.update) {
- this.update(watcher.value);
+ } else if (tagName.toLowerCase() === 'p') {
+ if (options.start) {
+ options.start(tagName, [], false, start, end);
}
- }
- };
-
- /**
- * Setup all param attributes, e.g. track-by,
- * transition-mode, etc...
- */
-
- Directive.prototype._setupParams = function () {
- if (!this.params) {
- return;
- }
- var params = this.params;
- // swap the params array with a fresh object.
- this.params = Object.create(null);
- var i = params.length;
- var key, val, mappedKey;
- while (i--) {
- key = hyphenate(params[i]);
- mappedKey = camelize(key);
- val = getBindAttr(this.el, key);
- if (val != null) {
- // dynamic
- this._setupParamWatcher(mappedKey, val);
- } else {
- // static
- val = getAttr(this.el, key);
- if (val != null) {
- this.params[mappedKey] = val === '' ? true : val;
- }
+ if (options.end) {
+ options.end(tagName, start, end);
}
}
- };
+ }
+}
- /**
- * Setup a watcher for a dynamic param.
- *
- * @param {String} key
- * @param {String} expression
- */
+/* */
- Directive.prototype._setupParamWatcher = function (key, expression) {
- var self = this;
- var called = false;
- var unwatch = (this._scope || this.vm).$watch(expression, function (val, oldVal) {
- self.params[key] = val;
- // since we are in immediate mode,
- // only call the param change callbacks if this is not the first update.
- if (called) {
- var cb = self.paramWatchers && self.paramWatchers[key];
- if (cb) {
- cb.call(self, val, oldVal);
- }
+function parseFilters (exp) {
+ var inSingle = false;
+ var inDouble = false;
+ var curly = 0;
+ var square = 0;
+ var paren = 0;
+ var lastFilterIndex = 0;
+ var c, prev, i, expression, filters;
+
+ for (i = 0; i < exp.length; i++) {
+ prev = c;
+ c = exp.charCodeAt(i);
+ if (inSingle) {
+ // check single quote
+ if (c === 0x27 && prev !== 0x5C) { inSingle = !inSingle; }
+ } else if (inDouble) {
+ // check double quote
+ if (c === 0x22 && prev !== 0x5C) { inDouble = !inDouble; }
+ } else if (
+ c === 0x7C && // pipe
+ exp.charCodeAt(i + 1) !== 0x7C &&
+ exp.charCodeAt(i - 1) !== 0x7C &&
+ !curly && !square && !paren
+ ) {
+ if (expression === undefined) {
+ // first filter, end of expression
+ lastFilterIndex = i + 1;
+ expression = exp.slice(0, i).trim();
} else {
- called = true;
+ pushFilter();
}
- }, {
- immediate: true,
- user: false
- });(this._paramUnwatchFns || (this._paramUnwatchFns = [])).push(unwatch);
- };
-
- /**
- * Check if the directive is a function caller
- * and if the expression is a callable one. If both true,
- * we wrap up the expression and use it as the event
- * handler.
- *
- * e.g. on-click="a++"
- *
- * @return {Boolean}
- */
-
- Directive.prototype._checkStatement = function () {
- var expression = this.expression;
- if (expression && this.acceptStatement && !isSimplePath(expression)) {
- var fn = parseExpression(expression).get;
- var scope = this._scope || this.vm;
- var handler = function handler(e) {
- scope.$event = e;
- fn.call(scope, scope);
- scope.$event = null;
- };
- if (this.filters) {
- handler = scope._applyFilters(handler, null, this.filters);
+ } else {
+ switch (c) {
+ case 0x22: inDouble = true; break // "
+ case 0x27: inSingle = true; break // '
+ case 0x28: paren++; break // (
+ case 0x29: paren--; break // )
+ case 0x5B: square++; break // [
+ case 0x5D: square--; break // ]
+ case 0x7B: curly++; break // {
+ case 0x7D: curly--; break // }
}
- this.update(handler);
- return true;
- }
- };
-
- /**
- * Set the corresponding value with the setter.
- * This should only be used in two-way directives
- * e.g. v-model.
- *
- * @param {*} value
- * @public
- */
-
- Directive.prototype.set = function (value) {
- /* istanbul ignore else */
- if (this.twoWay) {
- this._withLock(function () {
- this._watcher.set(value);
- });
- } else if ('development' !== 'production') {
- warn('Directive.set() can only be used inside twoWay' + 'directives.');
}
- };
-
- /**
- * Execute a function while preventing that function from
- * triggering updates on this directive instance.
- *
- * @param {Function} fn
- */
-
- Directive.prototype._withLock = function (fn) {
- var self = this;
- self._locked = true;
- fn.call(self);
- nextTick(function () {
- self._locked = false;
- });
- };
-
- /**
- * Convenience method that attaches a DOM event listener
- * to the directive element and autometically tears it down
- * during unbind.
- *
- * @param {String} event
- * @param {Function} handler
- * @param {Boolean} [useCapture]
- */
+ }
- Directive.prototype.on = function (event, handler, useCapture) {
- on(this.el, event, handler, useCapture);(this._listeners || (this._listeners = [])).push([event, handler]);
- };
+ if (expression === undefined) {
+ expression = exp.slice(0, i).trim();
+ } else if (lastFilterIndex !== 0) {
+ pushFilter();
+ }
- /**
- * Teardown the watcher and call unbind.
- */
+ function pushFilter () {
+ (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim());
+ lastFilterIndex = i + 1;
+ }
- Directive.prototype._teardown = function () {
- if (this._bound) {
- this._bound = false;
- if (this.unbind) {
- this.unbind();
- }
- if (this._watcher) {
- this._watcher.teardown();
- }
- var listeners = this._listeners;
- var i;
- if (listeners) {
- i = listeners.length;
- while (i--) {
- off(this.el, listeners[i][0], listeners[i][1]);
- }
- }
- var unwatchFns = this._paramUnwatchFns;
- if (unwatchFns) {
- i = unwatchFns.length;
- while (i--) {
- unwatchFns[i]();
- }
- }
- if ('development' !== 'production' && this.el) {
- this.el._vue_directives.$remove(this);
- }
- this.vm = this.el = this._watcher = this._listeners = null;
+ if (filters) {
+ for (i = 0; i < filters.length; i++) {
+ expression = wrapFilter(expression, filters[i]);
}
- };
+ }
- function lifecycleMixin (Vue) {
- /**
- * Update v-ref for component.
- *
- * @param {Boolean} remove
- */
-
- Vue.prototype._updateRef = function (remove) {
- var ref = this.$options._ref;
- if (ref) {
- var refs = (this._scope || this._context).$refs;
- if (remove) {
- if (refs[ref] === this) {
- refs[ref] = null;
- }
- } else {
- refs[ref] = this;
- }
- }
- };
+ return expression
+}
- /**
- * Transclude, compile and link element.
- *
- * If a pre-compiled linker is available, that means the
- * passed in element will be pre-transcluded and compiled
- * as well - all we need to do is to call the linker.
- *
- * Otherwise we need to call transclude/compile/link here.
- *
- * @param {Element} el
- */
-
- Vue.prototype._compile = function (el) {
- var options = this.$options;
-
- // transclude and init element
- // transclude can potentially replace original
- // so we need to keep reference; this step also injects
- // the template and caches the original attributes
- // on the container node and replacer node.
- var original = el;
- el = transclude(el, options);
- this._initElement(el);
-
- // handle v-pre on root node (#2026)
- if (el.nodeType === 1 && getAttr(el, 'v-pre') !== null) {
- return;
- }
-
- // root is always compiled per-instance, because
- // container attrs and props can be different every time.
- var contextOptions = this._context && this._context.$options;
- var rootLinker = compileRoot(el, options, contextOptions);
-
- // resolve slot distribution
- resolveSlots(this, options._content);
-
- // compile and link the rest
- var contentLinkFn;
- var ctor = this.constructor;
- // component compilation can be cached
- // as long as it's not using inline-template
- if (options._linkerCachable) {
- contentLinkFn = ctor.linker;
- if (!contentLinkFn) {
- contentLinkFn = ctor.linker = compile(el, options);
- }
+function wrapFilter (exp, filter) {
+ var i = filter.indexOf('(');
+ if (i < 0) {
+ // _f: resolveFilter
+ return ("_f(\"" + filter + "\")(" + exp + ")")
+ } else {
+ var name = filter.slice(0, i);
+ var args = filter.slice(i + 1);
+ return ("_f(\"" + name + "\")(" + exp + "," + args)
+ }
+}
+
+/* */
+
+var defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g;
+var regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g;
+
+var buildRegex = cached(function (delimiters) {
+ var open = delimiters[0].replace(regexEscapeRE, '\\$&');
+ var close = delimiters[1].replace(regexEscapeRE, '\\$&');
+ return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
+});
+
+function parseText (
+ text,
+ delimiters
+) {
+ var tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE;
+ if (!tagRE.test(text)) {
+ return
+ }
+ var tokens = [];
+ var lastIndex = tagRE.lastIndex = 0;
+ var match, index;
+ while ((match = tagRE.exec(text))) {
+ index = match.index;
+ // push text token
+ if (index > lastIndex) {
+ tokens.push(JSON.stringify(text.slice(lastIndex, index)));
+ }
+ // tag token
+ var exp = parseFilters(match[1].trim());
+ tokens.push(("_s(" + exp + ")"));
+ lastIndex = index + match[0].length;
+ }
+ if (lastIndex < text.length) {
+ tokens.push(JSON.stringify(text.slice(lastIndex)));
+ }
+ return tokens.join('+')
+}
+
+/* */
+
+function baseWarn (msg) {
+ console.error(("[Vue parser]: " + msg));
+}
+
+function pluckModuleFunction (
+ modules,
+ key
+) {
+ return modules
+ ? modules.map(function (m) { return m[key]; }).filter(function (_) { return _; })
+ : []
+}
+
+function addProp (el, name, value) {
+ (el.props || (el.props = [])).push({ name: name, value: value });
+}
+
+function addAttr (el, name, value) {
+ (el.attrs || (el.attrs = [])).push({ name: name, value: value });
+}
+
+function addDirective (
+ el,
+ name,
+ rawName,
+ value,
+ arg,
+ modifiers
+) {
+ (el.directives || (el.directives = [])).push({ name: name, rawName: rawName, value: value, arg: arg, modifiers: modifiers });
+}
+
+function addHandler (
+ el,
+ name,
+ value,
+ modifiers,
+ important
+) {
+ // check capture modifier
+ if (modifiers && modifiers.capture) {
+ delete modifiers.capture;
+ name = '!' + name; // mark the event as captured
+ }
+ var events;
+ if (modifiers && modifiers.native) {
+ delete modifiers.native;
+ events = el.nativeEvents || (el.nativeEvents = {});
+ } else {
+ events = el.events || (el.events = {});
+ }
+ var newHandler = { value: value, modifiers: modifiers };
+ var handlers = events[name];
+ /* istanbul ignore if */
+ if (Array.isArray(handlers)) {
+ important ? handlers.unshift(newHandler) : handlers.push(newHandler);
+ } else if (handlers) {
+ events[name] = important ? [newHandler, handlers] : [handlers, newHandler];
+ } else {
+ events[name] = newHandler;
+ }
+}
+
+function getBindingAttr (
+ el,
+ name,
+ getStatic
+) {
+ var dynamicValue =
+ getAndRemoveAttr(el, ':' + name) ||
+ getAndRemoveAttr(el, 'v-bind:' + name);
+ if (dynamicValue != null) {
+ return dynamicValue
+ } else if (getStatic !== false) {
+ var staticValue = getAndRemoveAttr(el, name);
+ if (staticValue != null) {
+ return JSON.stringify(staticValue)
+ }
+ }
+}
+
+function getAndRemoveAttr (el, name) {
+ var val;
+ if ((val = el.attrsMap[name]) != null) {
+ var list = el.attrsList;
+ for (var i = 0, l = list.length; i < l; i++) {
+ if (list[i].name === name) {
+ list.splice(i, 1);
+ break
+ }
+ }
+ }
+ return val
+}
+
+/* */
+
+var dirRE = /^v-|^@|^:/;
+var forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/;
+var forIteratorRE = /\(([^,]*),([^,]*)(?:,([^,]*))?\)/;
+var bindRE = /^:|^v-bind:/;
+var onRE = /^@|^v-on:/;
+var argRE = /:(.*)$/;
+var modifierRE = /\.[^\.]+/g;
+var specialNewlineRE = /\u2028|\u2029/g;
+
+var decodeHTMLCached = cached(decode);
+
+// configurable state
+var warn$1;
+var platformGetTagNamespace;
+var platformMustUseProp;
+var platformIsPreTag;
+var preTransforms;
+var transforms;
+var postTransforms;
+var delimiters;
+
+/**
+ * Convert HTML string to AST.
+ */
+function parse (
+ template,
+ options
+) {
+ warn$1 = options.warn || baseWarn;
+ platformGetTagNamespace = options.getTagNamespace || no;
+ platformMustUseProp = options.mustUseProp || no;
+ platformIsPreTag = options.isPreTag || no;
+ preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');
+ transforms = pluckModuleFunction(options.modules, 'transformNode');
+ postTransforms = pluckModuleFunction(options.modules, 'postTransformNode');
+ delimiters = options.delimiters;
+ var stack = [];
+ var preserveWhitespace = options.preserveWhitespace !== false;
+ var root;
+ var currentParent;
+ var inVPre = false;
+ var inPre = false;
+ var warned = false;
+ parseHTML(template, {
+ expectHTML: options.expectHTML,
+ isUnaryTag: options.isUnaryTag,
+ shouldDecodeNewlines: options.shouldDecodeNewlines,
+ start: function start (tag, attrs, unary) {
+ // check namespace.
+ // inherit parent ns if there is one
+ var ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag);
+
+ // handle IE svg bug
+ /* istanbul ignore if */
+ if (options.isIE && ns === 'svg') {
+ attrs = guardIESVGBug(attrs);
}
- // link phase
- // make sure to link root with prop scope!
- var rootUnlinkFn = rootLinker(this, el, this._scope);
- var contentUnlinkFn = contentLinkFn ? contentLinkFn(this, el) : compile(el, options)(this, el);
-
- // register composite unlink function
- // to be called during instance destruction
- this._unlinkFn = function () {
- rootUnlinkFn();
- // passing destroying: true to avoid searching and
- // splicing the directives
- contentUnlinkFn(true);
+ var element = {
+ type: 1,
+ tag: tag,
+ attrsList: attrs,
+ attrsMap: makeAttrsMap(attrs, options.isIE),
+ parent: currentParent,
+ children: []
};
-
- // finally replace original
- if (options.replace) {
- replace(original, el);
+ if (ns) {
+ element.ns = ns;
}
- this._isCompiled = true;
- this._callHook('compiled');
- };
-
- /**
- * Initialize instance element. Called in the public
- * $mount() method.
- *
- * @param {Element} el
- */
-
- Vue.prototype._initElement = function (el) {
- if (isFragment(el)) {
- this._isFragment = true;
- this.$el = this._fragmentStart = el.firstChild;
- this._fragmentEnd = el.lastChild;
- // set persisted text anchors to empty
- if (this._fragmentStart.nodeType === 3) {
- this._fragmentStart.data = this._fragmentEnd.data = '';
- }
- this._fragment = el;
- } else {
- this.$el = el;
+ if ("client" !== 'server' && isForbiddenTag(element)) {
+ element.forbidden = true;
+ "development" !== 'production' && warn$1(
+ 'Templates should only be responsible for mapping the state to the ' +
+ 'UI. Avoid placing tags with side-effects in your templates, such as ' +
+ "<" + tag + ">."
+ );
}
- this.$el.__vue__ = this;
- this._callHook('beforeCompile');
- };
- /**
- * Create and bind a directive to an element.
- *
- * @param {Object} descriptor - parsed directive descriptor
- * @param {Node} node - target node
- * @param {Vue} [host] - transclusion host component
- * @param {Object} [scope] - v-for scope
- * @param {Fragment} [frag] - owner fragment
- */
-
- Vue.prototype._bindDir = function (descriptor, node, host, scope, frag) {
- this._directives.push(new Directive(descriptor, this, node, host, scope, frag));
- };
-
- /**
- * Teardown an instance, unobserves the data, unbind all the
- * directives, turn off all the event listeners, etc.
- *
- * @param {Boolean} remove - whether to remove the DOM node.
- * @param {Boolean} deferCleanup - if true, defer cleanup to
- * be called later
- */
-
- Vue.prototype._destroy = function (remove, deferCleanup) {
- if (this._isBeingDestroyed) {
- if (!deferCleanup) {
- this._cleanup();
- }
- return;
+ // apply pre-transforms
+ for (var i = 0; i < preTransforms.length; i++) {
+ preTransforms[i](element, options);
}
- var destroyReady;
- var pendingRemoval;
-
- var self = this;
- // Cleanup should be called either synchronously or asynchronoysly as
- // callback of this.$remove(), or if remove and deferCleanup are false.
- // In any case it should be called after all other removing, unbinding and
- // turning of is done
- var cleanupIfPossible = function cleanupIfPossible() {
- if (destroyReady && !pendingRemoval && !deferCleanup) {
- self._cleanup();
+ if (!inVPre) {
+ processPre(element);
+ if (element.pre) {
+ inVPre = true;
}
- };
-
- // remove DOM element
- if (remove && this.$el) {
- pendingRemoval = true;
- this.$remove(function () {
- pendingRemoval = false;
- cleanupIfPossible();
- });
- }
-
- this._callHook('beforeDestroy');
- this._isBeingDestroyed = true;
- var i;
- // remove self from parent. only necessary
- // if parent is not being destroyed as well.
- var parent = this.$parent;
- if (parent && !parent._isBeingDestroyed) {
- parent.$children.$remove(this);
- // unregister ref (remove: true)
- this._updateRef(true);
- }
- // destroy all children.
- i = this.$children.length;
- while (i--) {
- this.$children[i].$destroy();
}
- // teardown props
- if (this._propsUnlinkFn) {
- this._propsUnlinkFn();
+ if (platformIsPreTag(element.tag)) {
+ inPre = true;
}
- // teardown all directives. this also tearsdown all
- // directive-owned watchers.
- if (this._unlinkFn) {
- this._unlinkFn();
- }
- i = this._watchers.length;
- while (i--) {
- this._watchers[i].teardown();
- }
- // remove reference to self on $el
- if (this.$el) {
- this.$el.__vue__ = null;
- }
-
- destroyReady = true;
- cleanupIfPossible();
- };
-
- /**
- * Clean up to ensure garbage collection.
- * This is called after the leave transition if there
- * is any.
- */
-
- Vue.prototype._cleanup = function () {
- if (this._isDestroyed) {
- return;
- }
- // remove self from owner fragment
- // do it in cleanup so that we can call $destroy with
- // defer right when a fragment is about to be removed.
- if (this._frag) {
- this._frag.children.$remove(this);
- }
- // remove reference from data ob
- // frozen object may not have observer.
- if (this._data && this._data.__ob__) {
- this._data.__ob__.removeVm(this);
- }
- // Clean up references to private properties and other
- // instances. preserve reference to _data so that proxy
- // accessors still work. The only potential side effect
- // here is that mutating the instance after it's destroyed
- // may affect the state of other components that are still
- // observing the same object, but that seems to be a
- // reasonable responsibility for the user rather than
- // always throwing an error on them.
- this.$el = this.$parent = this.$root = this.$children = this._watchers = this._context = this._scope = this._directives = null;
- // call the last hook...
- this._isDestroyed = true;
- this._callHook('destroyed');
- // turn off all instance listeners.
- this.$off();
- };
- }
-
- function miscMixin (Vue) {
- /**
- * Apply a list of filter (descriptors) to a value.
- * Using plain for loops here because this will be called in
- * the getter of any watcher with filters so it is very
- * performance sensitive.
- *
- * @param {*} value
- * @param {*} [oldValue]
- * @param {Array} filters
- * @param {Boolean} write
- * @return {*}
- */
-
- Vue.prototype._applyFilters = function (value, oldValue, filters, write) {
- var filter, fn, args, arg, offset, i, l, j, k;
- for (i = 0, l = filters.length; i < l; i++) {
- filter = filters[write ? l - i - 1 : i];
- fn = resolveAsset(this.$options, 'filters', filter.name, true);
- if (!fn) continue;
- fn = write ? fn.write : fn.read || fn;
- if (typeof fn !== 'function') continue;
- args = write ? [value, oldValue] : [value];
- offset = write ? 2 : 1;
- if (filter.args) {
- for (j = 0, k = filter.args.length; j < k; j++) {
- arg = filter.args[j];
- args[j + offset] = arg.dynamic ? this.$get(arg.value) : arg.value;
+ if (inVPre) {
+ processRawAttrs(element);
+ } else {
+ processFor(element);
+ processIf(element);
+ processOnce(element);
+ processKey(element);
+
+ // determine whether this is a plain element after
+ // removing structural attributes
+ element.plain = !element.key && !attrs.length;
+
+ processRef(element);
+ processSlot(element);
+ processComponent(element);
+ for (var i$1 = 0; i$1 < transforms.length; i$1++) {
+ transforms[i$1](element, options);
+ }
+ processAttrs(element);
+ }
+
+ function checkRootConstraints (el) {
+ {
+ if (el.tag === 'slot' || el.tag === 'template') {
+ warn$1(
+ "Cannot use <" + (el.tag) + "> as component root element because it may " +
+ 'contain multiple nodes:\n' + template
+ );
+ }
+ if (el.attrsMap.hasOwnProperty('v-for')) {
+ warn$1(
+ 'Cannot use v-for on stateful component root element because ' +
+ 'it renders multiple elements:\n' + template
+ );
}
}
- value = fn.apply(this, args);
}
- return value;
- };
- /**
- * Resolve a component, depending on whether the component
- * is defined normally or using an async factory function.
- * Resolves synchronously if already resolved, otherwise
- * resolves asynchronously and caches the resolved
- * constructor on the factory.
- *
- * @param {String|Function} value
- * @param {Function} cb
- */
-
- Vue.prototype._resolveComponent = function (value, cb) {
- var factory;
- if (typeof value === 'function') {
- factory = value;
- } else {
- factory = resolveAsset(this.$options, 'components', value, true);
- }
- /* istanbul ignore if */
- if (!factory) {
- return;
- }
- // async component factory
- if (!factory.options) {
- if (factory.resolved) {
- // cached
- cb(factory.resolved);
- } else if (factory.requested) {
- // pool callbacks
- factory.pendingCallbacks.push(cb);
+ // tree management
+ if (!root) {
+ root = element;
+ checkRootConstraints(root);
+ } else if ("development" !== 'production' && !stack.length && !warned) {
+ // allow 2 root elements with v-if and v-else
+ if (root.if && element.else) {
+ checkRootConstraints(element);
+ root.elseBlock = element;
} else {
- factory.requested = true;
- var cbs = factory.pendingCallbacks = [cb];
- factory.call(this, function resolve(res) {
- if (isPlainObject(res)) {
- res = Vue.extend(res);
- }
- // cache resolved
- factory.resolved = res;
- // invoke callbacks
- for (var i = 0, l = cbs.length; i < l; i++) {
- cbs[i](res);
- }
- }, function reject(reason) {
- 'development' !== 'production' && warn('Failed to resolve async component' + (typeof value === 'string' ? ': ' + value : '') + '. ' + (reason ? '\nReason: ' + reason : ''));
- });
+ warned = true;
+ warn$1(
+ ("Component template should contain exactly one root element:\n\n" + template)
+ );
}
- } else {
- // normal component
- cb(factory);
}
- };
- }
-
- var filterRE$1 = /[^|]\|[^|]/;
-
- function dataAPI (Vue) {
- /**
- * Get the value from an expression on this vm.
- *
- * @param {String} exp
- * @param {Boolean} [asStatement]
- * @return {*}
- */
-
- Vue.prototype.$get = function (exp, asStatement) {
- var res = parseExpression(exp);
- if (res) {
- if (asStatement) {
- var self = this;
- return function statementHandler() {
- self.$arguments = toArray(arguments);
- var result = res.get.call(self, self);
- self.$arguments = null;
- return result;
- };
+ if (currentParent && !element.forbidden) {
+ if (element.else) {
+ processElse(element, currentParent);
} else {
- try {
- return res.get.call(this, this);
- } catch (e) {}
+ currentParent.children.push(element);
+ element.parent = currentParent;
}
}
- };
-
- /**
- * Set the value from an expression on this vm.
- * The expression must be a valid left-hand
- * expression in an assignment.
- *
- * @param {String} exp
- * @param {*} val
- */
-
- Vue.prototype.$set = function (exp, val) {
- var res = parseExpression(exp, true);
- if (res && res.set) {
- res.set.call(this, this, val);
+ if (!unary) {
+ currentParent = element;
+ stack.push(element);
}
- };
-
- /**
- * Delete a property on the VM
- *
- * @param {String} key
- */
-
- Vue.prototype.$delete = function (key) {
- del(this._data, key);
- };
-
- /**
- * Watch an expression, trigger callback when its
- * value changes.
- *
- * @param {String|Function} expOrFn
- * @param {Function} cb
- * @param {Object} [options]
- * - {Boolean} deep
- * - {Boolean} immediate
- * @return {Function} - unwatchFn
- */
-
- Vue.prototype.$watch = function (expOrFn, cb, options) {
- var vm = this;
- var parsed;
- if (typeof expOrFn === 'string') {
- parsed = parseDirective(expOrFn);
- expOrFn = parsed.expression;
- }
- var watcher = new Watcher(vm, expOrFn, cb, {
- deep: options && options.deep,
- sync: options && options.sync,
- filters: parsed && parsed.filters,
- user: !options || options.user !== false
- });
- if (options && options.immediate) {
- cb.call(vm, watcher.value);
+ // apply post-transforms
+ for (var i$2 = 0; i$2 < postTransforms.length; i$2++) {
+ postTransforms[i$2](element, options);
}
- return function unwatchFn() {
- watcher.teardown();
- };
- };
+ },
- /**
- * Evaluate a text directive, including filters.
- *
- * @param {String} text
- * @param {Boolean} [asStatement]
- * @return {String}
- */
-
- Vue.prototype.$eval = function (text, asStatement) {
- // check for filters.
- if (filterRE$1.test(text)) {
- var dir = parseDirective(text);
- // the filter regex check might give false positive
- // for pipes inside strings, so it's possible that
- // we don't get any filters here
- var val = this.$get(dir.expression, asStatement);
- return dir.filters ? this._applyFilters(val, null, dir.filters) : val;
- } else {
- // no filter
- return this.$get(text, asStatement);
+ end: function end () {
+ // remove trailing whitespace
+ var element = stack[stack.length - 1];
+ var lastNode = element.children[element.children.length - 1];
+ if (lastNode && lastNode.type === 3 && lastNode.text === ' ') {
+ element.children.pop();
}
- };
-
- /**
- * Interpolate a piece of template text.
- *
- * @param {String} text
- * @return {String}
- */
-
- Vue.prototype.$interpolate = function (text) {
- var tokens = parseText(text);
- var vm = this;
- if (tokens) {
- if (tokens.length === 1) {
- return vm.$eval(tokens[0].value) + '';
- } else {
- return tokens.map(function (token) {
- return token.tag ? vm.$eval(token.value) : token.value;
- }).join('');
- }
- } else {
- return text;
+ // pop stack
+ stack.length -= 1;
+ currentParent = stack[stack.length - 1];
+ // check pre state
+ if (element.pre) {
+ inVPre = false;
}
- };
+ if (platformIsPreTag(element.tag)) {
+ inPre = false;
+ }
+ },
- /**
- * Log instance data as a plain JS object
- * so that it is easier to inspect in console.
- * This method assumes console is available.
- *
- * @param {String} [path]
- */
-
- Vue.prototype.$log = function (path) {
- var data = path ? getPath(this._data, path) : this._data;
- if (data) {
- data = clean(data);
- }
- // include computed fields
- if (!path) {
- var key;
- for (key in this.$options.computed) {
- data[key] = clean(this[key]);
- }
- if (this._props) {
- for (key in this._props) {
- data[key] = clean(this[key]);
- }
+ chars: function chars (text) {
+ if (!currentParent) {
+ if ("development" !== 'production' && !warned && text === template) {
+ warned = true;
+ warn$1(
+ 'Component template requires a root element, rather than just text:\n\n' + template
+ );
+ }
+ return
+ }
+ text = inPre || text.trim()
+ ? decodeHTMLCached(text)
+ // only preserve whitespace if its not right after a starting tag
+ : preserveWhitespace && currentParent.children.length ? ' ' : '';
+ if (text) {
+ var expression;
+ if (!inVPre && text !== ' ' && (expression = parseText(text, delimiters))) {
+ currentParent.children.push({
+ type: 2,
+ expression: expression,
+ text: text
+ });
+ } else {
+ // #3895 special character
+ text = text.replace(specialNewlineRE, '');
+ currentParent.children.push({
+ type: 3,
+ text: text
+ });
}
}
- console.log(data);
- };
-
- /**
- * "clean" a getter/setter converted object into a plain
- * object copy.
- *
- * @param {Object} - obj
- * @return {Object}
- */
-
- function clean(obj) {
- return JSON.parse(JSON.stringify(obj));
}
- }
-
- function domAPI (Vue) {
- /**
- * Convenience on-instance nextTick. The callback is
- * auto-bound to the instance, and this avoids component
- * modules having to rely on the global Vue.
- *
- * @param {Function} fn
- */
-
- Vue.prototype.$nextTick = function (fn) {
- nextTick(fn, this);
- };
-
- /**
- * Append instance to target
- *
- * @param {Node} target
- * @param {Function} [cb]
- * @param {Boolean} [withTransition] - defaults to true
- */
-
- Vue.prototype.$appendTo = function (target, cb, withTransition) {
- return insert(this, target, cb, withTransition, append, appendWithTransition);
- };
-
- /**
- * Prepend instance to target
- *
- * @param {Node} target
- * @param {Function} [cb]
- * @param {Boolean} [withTransition] - defaults to true
- */
-
- Vue.prototype.$prependTo = function (target, cb, withTransition) {
- target = query(target);
- if (target.hasChildNodes()) {
- this.$before(target.firstChild, cb, withTransition);
- } else {
- this.$appendTo(target, cb, withTransition);
- }
- return this;
- };
-
- /**
- * Insert instance before target
- *
- * @param {Node} target
- * @param {Function} [cb]
- * @param {Boolean} [withTransition] - defaults to true
- */
-
- Vue.prototype.$before = function (target, cb, withTransition) {
- return insert(this, target, cb, withTransition, beforeWithCb, beforeWithTransition);
- };
-
- /**
- * Insert instance after target
- *
- * @param {Node} target
- * @param {Function} [cb]
- * @param {Boolean} [withTransition] - defaults to true
- */
-
- Vue.prototype.$after = function (target, cb, withTransition) {
- target = query(target);
- if (target.nextSibling) {
- this.$before(target.nextSibling, cb, withTransition);
- } else {
- this.$appendTo(target.parentNode, cb, withTransition);
- }
- return this;
- };
-
- /**
- * Remove instance from DOM
- *
- * @param {Function} [cb]
- * @param {Boolean} [withTransition] - defaults to true
- */
-
- Vue.prototype.$remove = function (cb, withTransition) {
- if (!this.$el.parentNode) {
- return cb && cb();
- }
- var inDocument = this._isAttached && inDoc(this.$el);
- // if we are not in document, no need to check
- // for transitions
- if (!inDocument) withTransition = false;
- var self = this;
- var realCb = function realCb() {
- if (inDocument) self._callHook('detached');
- if (cb) cb();
+ });
+ return root
+}
+
+function processPre (el) {
+ if (getAndRemoveAttr(el, 'v-pre') != null) {
+ el.pre = true;
+ }
+}
+
+function processRawAttrs (el) {
+ var l = el.attrsList.length;
+ if (l) {
+ var attrs = el.attrs = new Array(l);
+ for (var i = 0; i < l; i++) {
+ attrs[i] = {
+ name: el.attrsList[i].name,
+ value: JSON.stringify(el.attrsList[i].value)
};
- if (this._isFragment) {
- removeNodeRange(this._fragmentStart, this._fragmentEnd, this, this._fragment, realCb);
- } else {
- var op = withTransition === false ? removeWithCb : removeWithTransition;
- op(this.$el, this, realCb);
- }
- return this;
- };
-
- /**
- * Shared DOM insertion function.
- *
- * @param {Vue} vm
- * @param {Element} target
- * @param {Function} [cb]
- * @param {Boolean} [withTransition]
- * @param {Function} op1 - op for non-transition insert
- * @param {Function} op2 - op for transition insert
- * @return vm
- */
-
- function insert(vm, target, cb, withTransition, op1, op2) {
- target = query(target);
- var targetIsDetached = !inDoc(target);
- var op = withTransition === false || targetIsDetached ? op1 : op2;
- var shouldCallHook = !targetIsDetached && !vm._isAttached && !inDoc(vm.$el);
- if (vm._isFragment) {
- mapNodeRange(vm._fragmentStart, vm._fragmentEnd, function (node) {
- op(node, target, vm);
- });
- cb && cb();
- } else {
- op(vm.$el, target, vm, cb);
- }
- if (shouldCallHook) {
- vm._callHook('attached');
- }
- return vm;
- }
-
- /**
- * Check for selectors
- *
- * @param {String|Element} el
- */
-
- function query(el) {
- return typeof el === 'string' ? document.querySelector(el) : el;
}
-
- /**
- * Append operation that takes a callback.
- *
- * @param {Node} el
- * @param {Node} target
- * @param {Vue} vm - unused
- * @param {Function} [cb]
- */
-
- function append(el, target, vm, cb) {
- target.appendChild(el);
- if (cb) cb();
+ } else if (!el.pre) {
+ // non root node in pre blocks with no attributes
+ el.plain = true;
+ }
+}
+
+function processKey (el) {
+ var exp = getBindingAttr(el, 'key');
+ if (exp) {
+ if ("development" !== 'production' && el.tag === 'template') {
+ warn$1("<template> cannot be keyed. Place the key on real elements instead.");
+ }
+ el.key = exp;
+ }
+}
+
+function processRef (el) {
+ var ref = getBindingAttr(el, 'ref');
+ if (ref) {
+ el.ref = ref;
+ el.refInFor = checkInFor(el);
+ }
+}
+
+function processFor (el) {
+ var exp;
+ if ((exp = getAndRemoveAttr(el, 'v-for'))) {
+ var inMatch = exp.match(forAliasRE);
+ if (!inMatch) {
+ "development" !== 'production' && warn$1(
+ ("Invalid v-for expression: " + exp)
+ );
+ return
+ }
+ el.for = inMatch[2].trim();
+ var alias = inMatch[1].trim();
+ var iteratorMatch = alias.match(forIteratorRE);
+ if (iteratorMatch) {
+ el.alias = iteratorMatch[1].trim();
+ el.iterator1 = iteratorMatch[2].trim();
+ if (iteratorMatch[3]) {
+ el.iterator2 = iteratorMatch[3].trim();
+ }
+ } else {
+ el.alias = alias;
}
+ }
+}
- /**
- * InsertBefore operation that takes a callback.
- *
- * @param {Node} el
- * @param {Node} target
- * @param {Vue} vm - unused
- * @param {Function} [cb]
- */
+function processIf (el) {
+ var exp = getAndRemoveAttr(el, 'v-if');
+ if (exp) {
+ el.if = exp;
+ }
+ if (getAndRemoveAttr(el, 'v-else') != null) {
+ el.else = true;
+ }
+}
- function beforeWithCb(el, target, vm, cb) {
- before(el, target);
- if (cb) cb();
- }
+function processElse (el, parent) {
+ var prev = findPrevElement(parent.children);
+ if (prev && prev.if) {
+ prev.elseBlock = el;
+ } else {
+ warn$1(
+ ("v-else used on element <" + (el.tag) + "> without corresponding v-if.")
+ );
+ }
+}
- /**
- * Remove operation that takes a callback.
- *
- * @param {Node} el
- * @param {Vue} vm - unused
- * @param {Function} [cb]
- */
+function processOnce (el) {
+ var once = getAndRemoveAttr(el, 'v-once');
+ if (once != null) {
+ el.once = true;
+ }
+}
- function removeWithCb(el, vm, cb) {
- remove(el);
- if (cb) cb();
+function processSlot (el) {
+ if (el.tag === 'slot') {
+ el.slotName = getBindingAttr(el, 'name');
+ } else {
+ var slotTarget = getBindingAttr(el, 'slot');
+ if (slotTarget) {
+ el.slotTarget = slotTarget;
}
}
+}
- function eventsAPI (Vue) {
- /**
- * Listen on the given `event` with `fn`.
- *
- * @param {String} event
- * @param {Function} fn
- */
-
- Vue.prototype.$on = function (event, fn) {
- (this._events[event] || (this._events[event] = [])).push(fn);
- modifyListenerCount(this, event, 1);
- return this;
- };
-
- /**
- * Adds an `event` listener that will be invoked a single
- * time then automatically removed.
- *
- * @param {String} event
- * @param {Function} fn
- */
-
- Vue.prototype.$once = function (event, fn) {
- var self = this;
- function on() {
- self.$off(event, on);
- fn.apply(this, arguments);
- }
- on.fn = fn;
- this.$on(event, on);
- return this;
- };
+function processComponent (el) {
+ var binding;
+ if ((binding = getBindingAttr(el, 'is'))) {
+ el.component = binding;
+ }
+ if (getAndRemoveAttr(el, 'inline-template') != null) {
+ el.inlineTemplate = true;
+ }
+}
- /**
- * Remove the given callback for `event` or all
- * registered callbacks.
- *
- * @param {String} event
- * @param {Function} fn
- */
-
- Vue.prototype.$off = function (event, fn) {
- var cbs;
- // all
- if (!arguments.length) {
- if (this.$parent) {
- for (event in this._events) {
- cbs = this._events[event];
- if (cbs) {
- modifyListenerCount(this, event, -cbs.length);
- }
- }
- }
- this._events = {};
- return this;
- }
- // specific event
- cbs = this._events[event];
- if (!cbs) {
- return this;
- }
- if (arguments.length === 1) {
- modifyListenerCount(this, event, -cbs.length);
- this._events[event] = null;
- return this;
- }
- // specific handler
- var cb;
- var i = cbs.length;
- while (i--) {
- cb = cbs[i];
- if (cb === fn || cb.fn === fn) {
- modifyListenerCount(this, event, -1);
- cbs.splice(i, 1);
- break;
+function processAttrs (el) {
+ var list = el.attrsList;
+ var i, l, name, rawName, value, arg, modifiers, isProp;
+ for (i = 0, l = list.length; i < l; i++) {
+ name = rawName = list[i].name;
+ value = list[i].value;
+ if (dirRE.test(name)) {
+ // mark element as dynamic
+ el.hasBindings = true;
+ // modifiers
+ modifiers = parseModifiers(name);
+ if (modifiers) {
+ name = name.replace(modifierRE, '');
+ }
+ if (bindRE.test(name)) { // v-bind
+ name = name.replace(bindRE, '');
+ if (modifiers && modifiers.prop) {
+ isProp = true;
+ name = camelize(name);
+ if (name === 'innerHtml') { name = 'innerHTML'; }
+ }
+ if (isProp || platformMustUseProp(name)) {
+ addProp(el, name, value);
+ } else {
+ addAttr(el, name, value);
}
- }
- return this;
- };
-
- /**
- * Trigger an event on self.
- *
- * @param {String|Object} event
- * @return {Boolean} shouldPropagate
- */
-
- Vue.prototype.$emit = function (event) {
- var isSource = typeof event === 'string';
- event = isSource ? event : event.name;
- var cbs = this._events[event];
- var shouldPropagate = isSource || !cbs;
- if (cbs) {
- cbs = cbs.length > 1 ? toArray(cbs) : cbs;
- // this is a somewhat hacky solution to the question raised
- // in #2102: for an inline component listener like <comp @test="doThis">,
- // the propagation handling is somewhat broken. Therefore we
- // need to treat these inline callbacks differently.
- var hasParentCbs = isSource && cbs.some(function (cb) {
- return cb._fromParent;
- });
- if (hasParentCbs) {
- shouldPropagate = false;
+ } else if (onRE.test(name)) { // v-on
+ name = name.replace(onRE, '');
+ addHandler(el, name, value, modifiers);
+ } else { // normal directives
+ name = name.replace(dirRE, '');
+ // parse arg
+ var argMatch = name.match(argRE);
+ if (argMatch && (arg = argMatch[1])) {
+ name = name.slice(0, -(arg.length + 1));
}
- var args = toArray(arguments, 1);
- for (var i = 0, l = cbs.length; i < l; i++) {
- var cb = cbs[i];
- var res = cb.apply(this, args);
- if (res === true && (!hasParentCbs || cb._fromParent)) {
- shouldPropagate = true;
- }
+ addDirective(el, name, rawName, value, arg, modifiers);
+ if ("development" !== 'production' && name === 'model') {
+ checkForAliasModel(el, value);
}
}
- return shouldPropagate;
- };
-
- /**
- * Recursively broadcast an event to all children instances.
- *
- * @param {String|Object} event
- * @param {...*} additional arguments
- */
-
- Vue.prototype.$broadcast = function (event) {
- var isSource = typeof event === 'string';
- event = isSource ? event : event.name;
- // if no child has registered for this event,
- // then there's no need to broadcast.
- if (!this._eventsCount[event]) return;
- var children = this.$children;
- var args = toArray(arguments);
- if (isSource) {
- // use object event to indicate non-source emit
- // on children
- args[0] = { name: event, source: this };
- }
- for (var i = 0, l = children.length; i < l; i++) {
- var child = children[i];
- var shouldPropagate = child.$emit.apply(child, args);
- if (shouldPropagate) {
- child.$broadcast.apply(child, args);
+ } else {
+ // literal attribute
+ {
+ var expression = parseText(value, delimiters);
+ if (expression) {
+ warn$1(
+ name + "=\"" + value + "\": " +
+ 'Interpolation inside attributes has been deprecated. ' +
+ 'Use v-bind or the colon shorthand instead.'
+ );
}
}
- return this;
- };
-
- /**
- * Recursively propagate an event up the parent chain.
- *
- * @param {String} event
- * @param {...*} additional arguments
- */
-
- Vue.prototype.$dispatch = function (event) {
- var shouldPropagate = this.$emit.apply(this, arguments);
- if (!shouldPropagate) return;
- var parent = this.$parent;
- var args = toArray(arguments);
- // use object event to indicate non-source emit
- // on parents
- args[0] = { name: event, source: this };
- while (parent) {
- shouldPropagate = parent.$emit.apply(parent, args);
- parent = shouldPropagate ? parent.$parent : null;
- }
- return this;
- };
-
- /**
- * Modify the listener counts on all parents.
- * This bookkeeping allows $broadcast to return early when
- * no child has listened to a certain event.
- *
- * @param {Vue} vm
- * @param {String} event
- * @param {Number} count
- */
-
- var hookRE = /^hook:/;
- function modifyListenerCount(vm, event, count) {
- var parent = vm.$parent;
- // hooks do not get broadcasted so no need
- // to do bookkeeping for them
- if (!parent || !count || hookRE.test(event)) return;
- while (parent) {
- parent._eventsCount[event] = (parent._eventsCount[event] || 0) + count;
- parent = parent.$parent;
- }
- }
- }
-
- function lifecycleAPI (Vue) {
- /**
- * Set instance target element and kick off the compilation
- * process. The passed in `el` can be a selector string, an
- * existing Element, or a DocumentFragment (for block
- * instances).
- *
- * @param {Element|DocumentFragment|string} el
- * @public
- */
-
- Vue.prototype.$mount = function (el) {
- if (this._isCompiled) {
- 'development' !== 'production' && warn('$mount() should be called only once.', this);
- return;
- }
- el = query(el);
- if (!el) {
- el = document.createElement('div');
- }
- this._compile(el);
- this._initDOMHooks();
- if (inDoc(this.$el)) {
- this._callHook('attached');
- ready.call(this);
- } else {
- this.$once('hook:attached', ready);
- }
- return this;
- };
-
- /**
- * Mark an instance as ready.
- */
-
- function ready() {
- this._isAttached = true;
- this._isReady = true;
- this._callHook('ready');
+ addAttr(el, name, JSON.stringify(value));
}
-
- /**
- * Teardown the instance, simply delegate to the internal
- * _destroy.
- *
- * @param {Boolean} remove
- * @param {Boolean} deferCleanup
- */
-
- Vue.prototype.$destroy = function (remove, deferCleanup) {
- this._destroy(remove, deferCleanup);
- };
-
- /**
- * Partially compile a piece of DOM and return a
- * decompile function.
- *
- * @param {Element|DocumentFragment} el
- * @param {Vue} [host]
- * @param {Object} [scope]
- * @param {Fragment} [frag]
- * @return {Function}
- */
-
- Vue.prototype.$compile = function (el, host, scope, frag) {
- return compile(el, this.$options, true)(this, el, host, scope, frag);
- };
}
+}
- /**
- * The exposed Vue constructor.
- *
- * API conventions:
- * - public API methods/properties are prefixed with `$`
- * - internal methods/properties are prefixed with `_`
- * - non-prefixed properties are assumed to be proxied user
- * data.
- *
- * @constructor
- * @param {Object} [options]
- * @public
- */
-
- function Vue(options) {
- this._init(options);
+function checkInFor (el) {
+ var parent = el;
+ while (parent) {
+ if (parent.for !== undefined) {
+ return true
+ }
+ parent = parent.parent;
}
+ return false
+}
- // install internals
- initMixin(Vue);
- stateMixin(Vue);
- eventsMixin(Vue);
- lifecycleMixin(Vue);
- miscMixin(Vue);
-
- // install instance APIs
- dataAPI(Vue);
- domAPI(Vue);
- eventsAPI(Vue);
- lifecycleAPI(Vue);
-
- var slot = {
-
- priority: SLOT,
- params: ['name'],
-
- bind: function bind() {
- // this was resolved during component transclusion
- var name = this.params.name || 'default';
- var content = this.vm._slotContents && this.vm._slotContents[name];
- if (!content || !content.hasChildNodes()) {
- this.fallback();
- } else {
- this.compile(content.cloneNode(true), this.vm._context, this.vm);
- }
- },
-
- compile: function compile(content, context, host) {
- if (content && context) {
- if (this.el.hasChildNodes() && content.childNodes.length === 1 && content.childNodes[0].nodeType === 1 && content.childNodes[0].hasAttribute('v-if')) {
- // if the inserted slot has v-if
- // inject fallback content as the v-else
- var elseBlock = document.createElement('template');
- elseBlock.setAttribute('v-else', '');
- elseBlock.innerHTML = this.el.innerHTML;
- // the else block should be compiled in child scope
- elseBlock._context = this.vm;
- content.appendChild(elseBlock);
- }
- var scope = host ? host._scope : this._scope;
- this.unlink = context.$compile(content, host, scope, this._frag);
- }
- if (content) {
- replace(this.el, content);
+function parseModifiers (name) {
+ var match = name.match(modifierRE);
+ if (match) {
+ var ret = {};
+ match.forEach(function (m) { ret[m.slice(1)] = true; });
+ return ret
+ }
+}
+
+function makeAttrsMap (attrs, isIE) {
+ var map = {};
+ for (var i = 0, l = attrs.length; i < l; i++) {
+ if ("development" !== 'production' && map[attrs[i].name] && !isIE) {
+ warn$1('duplicate attribute: ' + attrs[i].name);
+ }
+ map[attrs[i].name] = attrs[i].value;
+ }
+ return map
+}
+
+function findPrevElement (children) {
+ var i = children.length;
+ while (i--) {
+ if (children[i].tag) { return children[i] }
+ }
+}
+
+function isForbiddenTag (el) {
+ return (
+ el.tag === 'style' ||
+ (el.tag === 'script' && (
+ !el.attrsMap.type ||
+ el.attrsMap.type === 'text/javascript'
+ ))
+ )
+}
+
+var ieNSBug = /^xmlns:NS\d+/;
+var ieNSPrefix = /^NS\d+:/;
+
+/* istanbul ignore next */
+function guardIESVGBug (attrs) {
+ var res = [];
+ for (var i = 0; i < attrs.length; i++) {
+ var attr = attrs[i];
+ if (!ieNSBug.test(attr.name)) {
+ attr.name = attr.name.replace(ieNSPrefix, '');
+ res.push(attr);
+ }
+ }
+ return res
+}
+
+function checkForAliasModel (el, value) {
+ var _el = el;
+ while (_el) {
+ if (_el.for && _el.alias === value) {
+ warn$1(
+ "<" + (el.tag) + " v-model=\"" + value + "\">: " +
+ "You are binding v-model directly to a v-for iteration alias. " +
+ "This will not be able to modify the v-for source array because " +
+ "writing to the alias is like modifying a function local variable. " +
+ "Consider using an array of objects and use v-model on an object property instead."
+ );
+ }
+ _el = _el.parent;
+ }
+}
+
+/* */
+
+var isStaticKey;
+var isPlatformReservedTag;
+
+var genStaticKeysCached = cached(genStaticKeys$1);
+
+/**
+ * Goal of the optimizier: walk the generated template AST tree
+ * and detect sub-trees that are purely static, i.e. parts of
+ * the DOM that never needs to change.
+ *
+ * Once we detect these sub-trees, we can:
+ *
+ * 1. Hoist them into constants, so that we no longer need to
+ * create fresh nodes for them on each re-render;
+ * 2. Completely skip them in the patching process.
+ */
+function optimize (root, options) {
+ if (!root) { return }
+ isStaticKey = genStaticKeysCached(options.staticKeys || '');
+ isPlatformReservedTag = options.isReservedTag || (function () { return false; });
+ // first pass: mark all non-static nodes.
+ markStatic(root);
+ // second pass: mark static roots.
+ markStaticRoots(root, false);
+}
+
+function genStaticKeys$1 (keys) {
+ return makeMap(
+ 'type,tag,attrsList,attrsMap,plain,parent,children,attrs' +
+ (keys ? ',' + keys : '')
+ )
+}
+
+function markStatic (node) {
+ node.static = isStatic(node);
+ if (node.type === 1) {
+ for (var i = 0, l = node.children.length; i < l; i++) {
+ var child = node.children[i];
+ markStatic(child);
+ if (!child.static) {
+ node.static = false;
+ }
+ }
+ }
+}
+
+function markStaticRoots (node, isInFor) {
+ if (node.type === 1) {
+ if (node.once || node.static) {
+ node.staticRoot = true;
+ node.staticInFor = isInFor;
+ return
+ }
+ if (node.children) {
+ for (var i = 0, l = node.children.length; i < l; i++) {
+ markStaticRoots(node.children[i], isInFor || !!node.for);
+ }
+ }
+ }
+}
+
+function isStatic (node) {
+ if (node.type === 2) { // expression
+ return false
+ }
+ if (node.type === 3) { // text
+ return true
+ }
+ return !!(node.pre || (
+ !node.hasBindings && // no dynamic bindings
+ !node.if && !node.for && // not v-if or v-for or v-else
+ !isBuiltInTag(node.tag) && // not a built-in
+ isPlatformReservedTag(node.tag) && // not a component
+ !isDirectChildOfTemplateFor(node) &&
+ Object.keys(node).every(isStaticKey)
+ ))
+}
+
+function isDirectChildOfTemplateFor (node) {
+ while (node.parent) {
+ node = node.parent;
+ if (node.tag !== 'template') {
+ return false
+ }
+ if (node.for) {
+ return true
+ }
+ }
+ return false
+}
+
+/* */
+
+var simplePathRE = /^\s*[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*\s*$/;
+
+// keyCode aliases
+var keyCodes = {
+ esc: 27,
+ tab: 9,
+ enter: 13,
+ space: 32,
+ up: 38,
+ left: 37,
+ right: 39,
+ down: 40,
+ 'delete': [8, 46]
+};
+
+var modifierCode = {
+ stop: '$event.stopPropagation();',
+ prevent: '$event.preventDefault();',
+ self: 'if($event.target !== $event.currentTarget)return;'
+};
+
+function genHandlers (events, native) {
+ var res = native ? 'nativeOn:{' : 'on:{';
+ for (var name in events) {
+ res += "\"" + name + "\":" + (genHandler(events[name])) + ",";
+ }
+ return res.slice(0, -1) + '}'
+}
+
+function genHandler (
+ handler
+) {
+ if (!handler) {
+ return 'function(){}'
+ } else if (Array.isArray(handler)) {
+ return ("[" + (handler.map(genHandler).join(',')) + "]")
+ } else if (!handler.modifiers) {
+ return simplePathRE.test(handler.value)
+ ? handler.value
+ : ("function($event){" + (handler.value) + "}")
+ } else {
+ var code = '';
+ var keys = [];
+ for (var key in handler.modifiers) {
+ if (modifierCode[key]) {
+ code += modifierCode[key];
} else {
- remove(this.el);
- }
- },
-
- fallback: function fallback() {
- this.compile(extractContent(this.el, true), this.vm);
- },
-
- unbind: function unbind() {
- if (this.unlink) {
- this.unlink();
+ keys.push(key);
}
}
- };
-
- var partial = {
-
- priority: PARTIAL,
-
- params: ['name'],
-
- // watch changes to name for dynamic partials
- paramWatchers: {
- name: function name(value) {
- vIf.remove.call(this);
- if (value) {
- this.insert(value);
- }
- }
- },
+ if (keys.length) {
+ code = genKeyFilter(keys) + code;
+ }
+ var handlerCode = simplePathRE.test(handler.value)
+ ? handler.value + '($event)'
+ : handler.value;
+ return 'function($event){' + code + handlerCode + '}'
+ }
+}
- bind: function bind() {
- this.anchor = createAnchor('v-partial');
- replace(this.el, this.anchor);
- this.insert(this.params.name);
- },
+function genKeyFilter (keys) {
+ var code = keys.length === 1
+ ? normalizeKeyCode(keys[0])
+ : Array.prototype.concat.apply([], keys.map(normalizeKeyCode));
+ if (Array.isArray(code)) {
+ return ("if(" + (code.map(function (c) { return ("$event.keyCode!==" + c); }).join('&&')) + ")return;")
+ } else {
+ return ("if($event.keyCode!==" + code + ")return;")
+ }
+}
- insert: function insert(id) {
- var partial = resolveAsset(this.vm.$options, 'partials', id, true);
- if (partial) {
- this.factory = new FragmentFactory(this.vm, partial);
- vIf.insert.call(this);
- }
- },
+function normalizeKeyCode (key) {
+ return (
+ parseInt(key, 10) || // number keyCode
+ keyCodes[key] || // built-in alias
+ ("_k(" + (JSON.stringify(key)) + ")") // custom alias
+ )
+}
- unbind: function unbind() {
- if (this.frag) {
- this.frag.destroy();
- }
- }
- };
+/* */
- var elementDirectives = {
- slot: slot,
- partial: partial
+function bind$2 (el, dir) {
+ el.wrapData = function (code) {
+ return ("_b(" + code + "," + (dir.value) + (dir.modifiers && dir.modifiers.prop ? ',true' : '') + ")")
};
+}
+
+var baseDirectives = {
+ bind: bind$2,
+ cloak: noop
+};
+
+/* */
+
+// configurable state
+var warn$2;
+var transforms$1;
+var dataGenFns;
+var platformDirectives$1;
+var staticRenderFns;
+var currentOptions;
+
+function generate (
+ ast,
+ options
+) {
+ // save previous staticRenderFns so generate calls can be nested
+ var prevStaticRenderFns = staticRenderFns;
+ var currentStaticRenderFns = staticRenderFns = [];
+ currentOptions = options;
+ warn$2 = options.warn || baseWarn;
+ transforms$1 = pluckModuleFunction(options.modules, 'transformCode');
+ dataGenFns = pluckModuleFunction(options.modules, 'genData');
+ platformDirectives$1 = options.directives || {};
+ var code = ast ? genElement(ast) : '_h("div")';
+ staticRenderFns = prevStaticRenderFns;
+ return {
+ render: ("with(this){return " + code + "}"),
+ staticRenderFns: currentStaticRenderFns
+ }
+}
+
+function genElement (el) {
+ if (el.staticRoot && !el.staticProcessed) {
+ // hoist static sub-trees out
+ el.staticProcessed = true;
+ staticRenderFns.push(("with(this){return " + (genElement(el)) + "}"));
+ return ("_m(" + (staticRenderFns.length - 1) + (el.staticInFor ? ',true' : '') + ")")
+ } else if (el.for && !el.forProcessed) {
+ return genFor(el)
+ } else if (el.if && !el.ifProcessed) {
+ return genIf(el)
+ } else if (el.tag === 'template' && !el.slotTarget) {
+ return genChildren(el) || 'void 0'
+ } else if (el.tag === 'slot') {
+ return genSlot(el)
+ } else {
+ // component or element
+ var code;
+ if (el.component) {
+ code = genComponent(el);
+ } else {
+ var data = genData(el);
+ var children = el.inlineTemplate ? null : genChildren(el);
+ code = "_h('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
+ }
+ // module transforms
+ for (var i = 0; i < transforms$1.length; i++) {
+ code = transforms$1[i](el, code);
+ }
+ return code
+ }
+}
+
+function genIf (el) {
+ var exp = el.if;
+ el.ifProcessed = true; // avoid recursion
+ return ("(" + exp + ")?" + (genElement(el)) + ":" + (genElse(el)))
+}
+
+function genElse (el) {
+ return el.elseBlock
+ ? genElement(el.elseBlock)
+ : '_e()'
+}
+
+function genFor (el) {
+ var exp = el.for;
+ var alias = el.alias;
+ var iterator1 = el.iterator1 ? ("," + (el.iterator1)) : '';
+ var iterator2 = el.iterator2 ? ("," + (el.iterator2)) : '';
+ el.forProcessed = true; // avoid recursion
+ return "_l((" + exp + ")," +
+ "function(" + alias + iterator1 + iterator2 + "){" +
+ "return " + (genElement(el)) +
+ '})'
+}
+
+function genData (el) {
+ if (el.plain) {
+ return
+ }
+
+ var data = '{';
+
+ // directives first.
+ // directives may mutate the el's other properties before they are generated.
+ var dirs = genDirectives(el);
+ if (dirs) { data += dirs + ','; }
+
+ // key
+ if (el.key) {
+ data += "key:" + (el.key) + ",";
+ }
+ // ref
+ if (el.ref) {
+ data += "ref:" + (el.ref) + ",";
+ }
+ if (el.refInFor) {
+ data += "refInFor:true,";
+ }
+ // record original tag name for components using "is" attribute
+ if (el.component) {
+ data += "tag:\"" + (el.tag) + "\",";
+ }
+ // slot target
+ if (el.slotTarget) {
+ data += "slot:" + (el.slotTarget) + ",";
+ }
+ // module data generation functions
+ for (var i = 0; i < dataGenFns.length; i++) {
+ data += dataGenFns[i](el);
+ }
+ // attributes
+ if (el.attrs) {
+ data += "attrs:{" + (genProps(el.attrs)) + "},";
+ }
+ // DOM props
+ if (el.props) {
+ data += "domProps:{" + (genProps(el.props)) + "},";
+ }
+ // event handlers
+ if (el.events) {
+ data += (genHandlers(el.events)) + ",";
+ }
+ if (el.nativeEvents) {
+ data += (genHandlers(el.nativeEvents, true)) + ",";
+ }
+ // inline-template
+ if (el.inlineTemplate) {
+ var ast = el.children[0];
+ if ("development" !== 'production' && (
+ el.children.length > 1 || ast.type !== 1
+ )) {
+ warn$2('Inline-template components must have exactly one child element.');
+ }
+ if (ast.type === 1) {
+ var inlineRenderFns = generate(ast, currentOptions);
+ data += "inlineTemplate:{render:function(){" + (inlineRenderFns.render) + "},staticRenderFns:[" + (inlineRenderFns.staticRenderFns.map(function (code) { return ("function(){" + code + "}"); }).join(',')) + "]}";
+ }
+ }
+ data = data.replace(/,$/, '') + '}';
+ // v-bind data wrap
+ if (el.wrapData) {
+ data = el.wrapData(data);
+ }
+ return data
+}
+
+function genDirectives (el) {
+ var dirs = el.directives;
+ if (!dirs) { return }
+ var res = 'directives:[';
+ var hasRuntime = false;
+ var i, l, dir, needRuntime;
+ for (i = 0, l = dirs.length; i < l; i++) {
+ dir = dirs[i];
+ needRuntime = true;
+ var gen = platformDirectives$1[dir.name] || baseDirectives[dir.name];
+ if (gen) {
+ // compile-time directive that manipulates AST.
+ // returns true if it also needs a runtime counterpart.
+ needRuntime = !!gen(el, dir, warn$2);
+ }
+ if (needRuntime) {
+ hasRuntime = true;
+ res += "{name:\"" + (dir.name) + "\",rawName:\"" + (dir.rawName) + "\"" + (dir.value ? (",value:(" + (dir.value) + "),expression:" + (JSON.stringify(dir.value))) : '') + (dir.arg ? (",arg:\"" + (dir.arg) + "\"") : '') + (dir.modifiers ? (",modifiers:" + (JSON.stringify(dir.modifiers))) : '') + "},";
+ }
+ }
+ if (hasRuntime) {
+ return res.slice(0, -1) + ']'
+ }
+}
- var convertArray = vFor._postProcess;
-
- /**
- * Limit filter for arrays
- *
- * @param {Number} n
- * @param {Number} offset (Decimal expected)
- */
-
- function limitBy(arr, n, offset) {
- offset = offset ? parseInt(offset, 10) : 0;
- n = toNumber(n);
- return typeof n === 'number' ? arr.slice(offset, offset + n) : arr;
+function genChildren (el) {
+ if (el.children.length) {
+ return '[' + el.children.map(genNode).join(',') + ']'
}
+}
- /**
- * Filter filter for arrays
- *
- * @param {String} search
- * @param {String} [delimiter]
- * @param {String} ...dataKeys
- */
-
- function filterBy(arr, search, delimiter) {
- arr = convertArray(arr);
- if (search == null) {
- return arr;
- }
- if (typeof search === 'function') {
- return arr.filter(search);
- }
- // cast to lowercase string
- search = ('' + search).toLowerCase();
- // allow optional `in` delimiter
- // because why not
- var n = delimiter === 'in' ? 3 : 2;
- // extract and flatten keys
- var keys = Array.prototype.concat.apply([], toArray(arguments, n));
- var res = [];
- var item, key, val, j;
- for (var i = 0, l = arr.length; i < l; i++) {
- item = arr[i];
- val = item && item.$value || item;
- j = keys.length;
- if (j) {
- while (j--) {
- key = keys[j];
- if (key === '$key' && contains(item.$key, search) || contains(getPath(val, key), search)) {
- res.push(item);
- break;
+function genNode (node) {
+ if (node.type === 1) {
+ return genElement(node)
+ } else {
+ return genText(node)
+ }
+}
+
+function genText (text) {
+ return text.type === 2
+ ? text.expression // no need for () because already wrapped in _s()
+ : JSON.stringify(text.text)
+}
+
+function genSlot (el) {
+ var slotName = el.slotName || '"default"';
+ var children = genChildren(el);
+ return children
+ ? ("_t(" + slotName + "," + children + ")")
+ : ("_t(" + slotName + ")")
+}
+
+function genComponent (el) {
+ var children = el.inlineTemplate ? null : genChildren(el);
+ return ("_h(" + (el.component) + "," + (genData(el)) + (children ? ("," + children) : '') + ")")
+}
+
+function genProps (props) {
+ var res = '';
+ for (var i = 0; i < props.length; i++) {
+ var prop = props[i];
+ res += "\"" + (prop.name) + "\":" + (prop.value) + ",";
+ }
+ return res.slice(0, -1)
+}
+
+/* */
+
+/**
+ * Compile a template.
+ */
+function compile$1 (
+ template,
+ options
+) {
+ var ast = parse(template.trim(), options);
+ optimize(ast, options);
+ var code = generate(ast, options);
+ return {
+ ast: ast,
+ render: code.render,
+ staticRenderFns: code.staticRenderFns
+ }
+}
+
+/* */
+
+// operators like typeof, instanceof and in are allowed
+var prohibitedKeywordRE = new RegExp('\\b' + (
+ 'do,if,for,let,new,try,var,case,else,with,await,break,catch,class,const,' +
+ 'super,throw,while,yield,delete,export,import,return,switch,default,' +
+ 'extends,finally,continue,debugger,function,arguments'
+).split(',').join('\\b|\\b') + '\\b');
+// check valid identifier for v-for
+var identRE = /[A-Za-z_$][\w$]*/;
+// strip strings in expressions
+var stripStringRE = /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`/g;
+
+// detect problematic expressions in a template
+function detectErrors (ast) {
+ var errors = [];
+ if (ast) {
+ checkNode(ast, errors);
+ }
+ return errors
+}
+
+function checkNode (node, errors) {
+ if (node.type === 1) {
+ for (var name in node.attrsMap) {
+ if (dirRE.test(name)) {
+ var value = node.attrsMap[name];
+ if (value) {
+ if (name === 'v-for') {
+ checkFor(node, ("v-for=\"" + value + "\""), errors);
+ } else {
+ checkExpression(value, (name + "=\"" + value + "\""), errors);
}
}
- } else if (contains(item, search)) {
- res.push(item);
}
}
- return res;
+ if (node.children) {
+ for (var i = 0; i < node.children.length; i++) {
+ checkNode(node.children[i], errors);
+ }
+ }
+ } else if (node.type === 2) {
+ checkExpression(node.expression, node.text, errors);
}
+}
- /**
- * Filter filter for arrays
- *
- * @param {String|Array<String>|Function} ...sortKeys
- * @param {Number} [order]
- */
-
- function orderBy(arr) {
- var comparator = null;
- var sortKeys = undefined;
- arr = convertArray(arr);
+function checkFor (node, text, errors) {
+ checkExpression(node.for || '', text, errors);
+ checkIdentifier(node.alias, 'v-for alias', text, errors);
+ checkIdentifier(node.iterator1, 'v-for iterator', text, errors);
+ checkIdentifier(node.iterator2, 'v-for iterator', text, errors);
+}
- // determine order (last argument)
- var args = toArray(arguments, 1);
- var order = args[args.length - 1];
- if (typeof order === 'number') {
- order = order < 0 ? -1 : 1;
- args = args.length > 1 ? args.slice(0, -1) : args;
- } else {
- order = 1;
- }
+function checkIdentifier (ident, type, text, errors) {
+ if (typeof ident === 'string' && !identRE.test(ident)) {
+ errors.push(("- invalid " + type + " \"" + ident + "\" in expression: " + text));
+ }
+}
- // determine sortKeys & comparator
- var firstArg = args[0];
- if (!firstArg) {
- return arr;
- } else if (typeof firstArg === 'function') {
- // custom comparator
- comparator = function (a, b) {
- return firstArg(a, b) * order;
- };
+function checkExpression (exp, text, errors) {
+ try {
+ new Function(("return " + exp));
+ } catch (e) {
+ var keywordMatch = exp.replace(stripStringRE, '').match(prohibitedKeywordRE);
+ if (keywordMatch) {
+ errors.push(
+ "- avoid using JavaScript keyword as property name: " +
+ "\"" + (keywordMatch[0]) + "\" in expression " + text
+ );
} else {
- // string keys. flatten first
- sortKeys = Array.prototype.concat.apply([], args);
- comparator = function (a, b, i) {
- i = i || 0;
- return i >= sortKeys.length - 1 ? baseCompare(a, b, i) : baseCompare(a, b, i) || comparator(a, b, i + 1);
- };
- }
-
- function baseCompare(a, b, sortKeyIndex) {
- var sortKey = sortKeys[sortKeyIndex];
- if (sortKey) {
- if (sortKey !== '$key') {
- if (isObject(a) && '$value' in a) a = a.$value;
- if (isObject(b) && '$value' in b) b = b.$value;
- }
- a = isObject(a) ? getPath(a, sortKey) : a;
- b = isObject(b) ? getPath(b, sortKey) : b;
+ errors.push(("- invalid expression: " + text));
+ }
+ }
+}
+
+/* */
+
+function transformNode (el, options) {
+ var warn = options.warn || baseWarn;
+ var staticClass = getAndRemoveAttr(el, 'class');
+ if ("development" !== 'production' && staticClass) {
+ var expression = parseText(staticClass, options.delimiters);
+ if (expression) {
+ warn(
+ "class=\"" + staticClass + "\": " +
+ 'Interpolation inside attributes has been deprecated. ' +
+ 'Use v-bind or the colon shorthand instead.'
+ );
+ }
+ }
+ if (staticClass) {
+ el.staticClass = JSON.stringify(staticClass);
+ }
+ var classBinding = getBindingAttr(el, 'class', false /* getStatic */);
+ if (classBinding) {
+ el.classBinding = classBinding;
+ }
+}
+
+function genData$1 (el) {
+ var data = '';
+ if (el.staticClass) {
+ data += "staticClass:" + (el.staticClass) + ",";
+ }
+ if (el.classBinding) {
+ data += "class:" + (el.classBinding) + ",";
+ }
+ return data
+}
+
+var klass$1 = {
+ staticKeys: ['staticClass'],
+ transformNode: transformNode,
+ genData: genData$1
+};
+
+/* */
+
+function transformNode$1 (el) {
+ var styleBinding = getBindingAttr(el, 'style', false /* getStatic */);
+ if (styleBinding) {
+ el.styleBinding = styleBinding;
+ }
+}
+
+function genData$2 (el) {
+ return el.styleBinding
+ ? ("style:(" + (el.styleBinding) + "),")
+ : ''
+}
+
+var style$1 = {
+ transformNode: transformNode$1,
+ genData: genData$2
+};
+
+var modules$1 = [
+ klass$1,
+ style$1
+];
+
+/* */
+
+var warn$3;
+
+function model$1 (
+ el,
+ dir,
+ _warn
+) {
+ warn$3 = _warn;
+ var value = dir.value;
+ var modifiers = dir.modifiers;
+ var tag = el.tag;
+ var type = el.attrsMap.type;
+ {
+ var dynamicType = el.attrsMap['v-bind:type'] || el.attrsMap[':type'];
+ if (tag === 'input' && dynamicType) {
+ warn$3(
+ "<input :type=\"" + dynamicType + "\" v-model=\"" + value + "\">:\n" +
+ "v-model does not support dynamic input types. Use v-if branches instead."
+ );
+ }
+ }
+ if (tag === 'select') {
+ genSelect(el, value);
+ } else if (tag === 'input' && type === 'checkbox') {
+ genCheckboxModel(el, value);
+ } else if (tag === 'input' && type === 'radio') {
+ genRadioModel(el, value);
+ } else {
+ genDefaultModel(el, value, modifiers);
+ }
+ // ensure runtime directive metadata
+ return true
+}
+
+function genCheckboxModel (el, value) {
+ if ("development" !== 'production' &&
+ el.attrsMap.checked != null) {
+ warn$3(
+ "<" + (el.tag) + " v-model=\"" + value + "\" checked>:\n" +
+ "inline checked attributes will be ignored when using v-model. " +
+ 'Declare initial values in the component\'s data option instead.'
+ );
+ }
+ var valueBinding = getBindingAttr(el, 'value') || 'null';
+ var trueValueBinding = getBindingAttr(el, 'true-value') || 'true';
+ var falseValueBinding = getBindingAttr(el, 'false-value') || 'false';
+ addProp(el, 'checked',
+ "Array.isArray(" + value + ")" +
+ "?_i(" + value + "," + valueBinding + ")>-1" +
+ ":_q(" + value + "," + trueValueBinding + ")"
+ );
+ addHandler(el, 'change',
+ "var $$a=" + value + "," +
+ '$$el=$event.target,' +
+ "$$c=$$el.checked?(" + trueValueBinding + "):(" + falseValueBinding + ");" +
+ 'if(Array.isArray($$a)){' +
+ "var $$v=" + valueBinding + "," +
+ '$$i=_i($$a,$$v);' +
+ "if($$c){$$i<0&&(" + value + "=$$a.concat($$v))}" +
+ "else{$$i>-1&&(" + value + "=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}" +
+ "}else{" + value + "=$$c}",
+ null, true
+ );
+}
+
+function genRadioModel (el, value) {
+ if ("development" !== 'production' &&
+ el.attrsMap.checked != null) {
+ warn$3(
+ "<" + (el.tag) + " v-model=\"" + value + "\" checked>:\n" +
+ "inline checked attributes will be ignored when using v-model. " +
+ 'Declare initial values in the component\'s data option instead.'
+ );
+ }
+ var valueBinding = getBindingAttr(el, 'value') || 'null';
+ addProp(el, 'checked', ("_q(" + value + "," + valueBinding + ")"));
+ addHandler(el, 'change', (value + "=" + valueBinding), null, true);
+}
+
+function genDefaultModel (
+ el,
+ value,
+ modifiers
+) {
+ {
+ if (el.tag === 'input' && el.attrsMap.value) {
+ warn$3(
+ "<" + (el.tag) + " v-model=\"" + value + "\" value=\"" + (el.attrsMap.value) + "\">:\n" +
+ 'inline value attributes will be ignored when using v-model. ' +
+ 'Declare initial values in the component\'s data option instead.'
+ );
+ }
+ if (el.tag === 'textarea' && el.children.length) {
+ warn$3(
+ "<textarea v-model=\"" + value + "\">:\n" +
+ 'inline content inside <textarea> will be ignored when using v-model. ' +
+ 'Declare initial values in the component\'s data option instead.'
+ );
+ }
+ }
+
+ var type = el.attrsMap.type;
+ var ref = modifiers || {};
+ var lazy = ref.lazy;
+ var number = ref.number;
+ var trim = ref.trim;
+ var event = lazy || (isIE && type === 'range') ? 'change' : 'input';
+ var needCompositionGuard = !lazy && type !== 'range';
+ var isNative = el.tag === 'input' || el.tag === 'textarea';
+
+ var valueExpression = isNative
+ ? ("$event.target.value" + (trim ? '.trim()' : ''))
+ : "$event";
+ var code = number || type === 'number'
+ ? (value + "=_n(" + valueExpression + ")")
+ : (value + "=" + valueExpression);
+ if (isNative && needCompositionGuard) {
+ code = "if($event.target.composing)return;" + code;
+ }
+ // inputs with type="file" are read only and setting the input's
+ // value will throw an error.
+ if ("development" !== 'production' &&
+ type === 'file') {
+ warn$3(
+ "<" + (el.tag) + " v-model=\"" + value + "\" type=\"file\">:\n" +
+ "File inputs are read only. Use a v-on:change listener instead."
+ );
+ }
+ addProp(el, 'value', isNative ? ("_s(" + value + ")") : ("(" + value + ")"));
+ addHandler(el, event, code, null, true);
+}
+
+function genSelect (el, value) {
+ {
+ el.children.some(checkOptionWarning);
+ }
+ var code = value + "=Array.prototype.filter" +
+ ".call($event.target.options,function(o){return o.selected})" +
+ ".map(function(o){return \"_value\" in o ? o._value : o.value})" +
+ (el.attrsMap.multiple == null ? '[0]' : '');
+ addHandler(el, 'change', code, null, true);
+}
+
+function checkOptionWarning (option) {
+ if (option.type === 1 &&
+ option.tag === 'option' &&
+ option.attrsMap.selected != null) {
+ warn$3(
+ "<select v-model=\"" + (option.parent.attrsMap['v-model']) + "\">:\n" +
+ 'inline selected attributes on <option> will be ignored when using v-model. ' +
+ 'Declare initial values in the component\'s data option instead.'
+ );
+ return true
+ }
+ return false
+}
+
+/* */
+
+function text (el, dir) {
+ if (dir.value) {
+ addProp(el, 'textContent', ("_s(" + (dir.value) + ")"));
+ }
+}
+
+/* */
+
+function html (el, dir) {
+ if (dir.value) {
+ addProp(el, 'innerHTML', ("_s(" + (dir.value) + ")"));
+ }
+}
+
+var directives$1 = {
+ model: model$1,
+ text: text,
+ html: html
+};
+
+/* */
+
+var cache = Object.create(null);
+
+var baseOptions = {
+ isIE: isIE,
+ expectHTML: true,
+ modules: modules$1,
+ staticKeys: genStaticKeys(modules$1),
+ directives: directives$1,
+ isReservedTag: isReservedTag,
+ isUnaryTag: isUnaryTag,
+ mustUseProp: mustUseProp,
+ getTagNamespace: getTagNamespace,
+ isPreTag: isPreTag
+};
+
+function compile$$1 (
+ template,
+ options
+) {
+ options = options
+ ? extend(extend({}, baseOptions), options)
+ : baseOptions;
+ return compile$1(template, options)
+}
+
+function compileToFunctions (
+ template,
+ options,
+ vm
+) {
+ var _warn = (options && options.warn) || warn;
+ // detect possible CSP restriction
+ /* istanbul ignore if */
+ {
+ try {
+ new Function('return 1');
+ } catch (e) {
+ if (e.toString().match(/unsafe-eval|CSP/)) {
+ _warn(
+ 'It seems you are using the standalone build of Vue.js in an ' +
+ 'environment with Content Security Policy that prohibits unsafe-eval. ' +
+ 'The template compiler cannot work in this environment. Consider ' +
+ 'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
+ 'templates into render functions.'
+ );
}
- return a === b ? 0 : a > b ? order : -order;
}
-
- // sort on a copy to avoid mutating original array
- return arr.slice().sort(comparator);
}
-
- /**
- * String contain helper
- *
- * @param {*} val
- * @param {String} search
- */
-
- function contains(val, search) {
- var i;
- if (isPlainObject(val)) {
- var keys = Object.keys(val);
- i = keys.length;
- while (i--) {
- if (contains(val[keys[i]], search)) {
- return true;
- }
- }
- } else if (isArray(val)) {
- i = val.length;
- while (i--) {
- if (contains(val[i], search)) {
- return true;
- }
- }
- } else if (val != null) {
- return val.toString().toLowerCase().indexOf(search) > -1;
+ var key = options && options.delimiters
+ ? String(options.delimiters) + template
+ : template;
+ if (cache[key]) {
+ return cache[key]
+ }
+ var res = {};
+ var compiled = compile$$1(template, options);
+ res.render = makeFunction(compiled.render);
+ var l = compiled.staticRenderFns.length;
+ res.staticRenderFns = new Array(l);
+ for (var i = 0; i < l; i++) {
+ res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i]);
+ }
+ {
+ if (res.render === noop || res.staticRenderFns.some(function (fn) { return fn === noop; })) {
+ _warn(
+ "failed to compile template:\n\n" + template + "\n\n" +
+ detectErrors(compiled.ast).join('\n') +
+ '\n\n',
+ vm
+ );
}
}
+ return (cache[key] = res)
+}
- var digitsRE = /(\d{3})(?=\d)/g;
-
- // asset collections must be a plain object.
- var filters = {
-
- orderBy: orderBy,
- filterBy: filterBy,
- limitBy: limitBy,
-
- /**
- * Stringify value.
- *
- * @param {Number} indent
- */
-
- json: {
- read: function read(value, indent) {
- return typeof value === 'string' ? value : JSON.stringify(value, null, arguments.length > 1 ? indent : 2);
- },
- write: function write(value) {
- try {
- return JSON.parse(value);
- } catch (e) {
- return value;
- }
- }
- },
-
- /**
- * 'abc' => 'Abc'
- */
-
- capitalize: function capitalize(value) {
- if (!value && value !== 0) return '';
- value = value.toString();
- return value.charAt(0).toUpperCase() + value.slice(1);
- },
-
- /**
- * 'abc' => 'ABC'
- */
+function makeFunction (code) {
+ try {
+ return new Function(code)
+ } catch (e) {
+ return noop
+ }
+}
- uppercase: function uppercase(value) {
- return value || value === 0 ? value.toString().toUpperCase() : '';
- },
+/* */
- /**
- * 'AbC' => 'abc'
- */
+var idToTemplate = cached(function (id) {
+ var el = query(id);
+ return el && el.innerHTML
+});
- lowercase: function lowercase(value) {
- return value || value === 0 ? value.toString().toLowerCase() : '';
- },
+var mount = Vue$3.prototype.$mount;
+Vue$3.prototype.$mount = function (
+ el,
+ hydrating
+) {
+ el = el && query(el);
- /**
- * 12345 => $12,345.00
- *
- * @param {String} sign
- * @param {Number} decimals Decimal places
- */
-
- currency: function currency(value, _currency, decimals) {
- value = parseFloat(value);
- if (!isFinite(value) || !value && value !== 0) return '';
- _currency = _currency != null ? _currency : '$';
- decimals = decimals != null ? decimals : 2;
- var stringified = Math.abs(value).toFixed(decimals);
- var _int = decimals ? stringified.slice(0, -1 - decimals) : stringified;
- var i = _int.length % 3;
- var head = i > 0 ? _int.slice(0, i) + (_int.length > 3 ? ',' : '') : '';
- var _float = decimals ? stringified.slice(-1 - decimals) : '';
- var sign = value < 0 ? '-' : '';
- return sign + _currency + head + _int.slice(i).replace(digitsRE, '$1,') + _float;
- },
+ /* istanbul ignore if */
+ if (el === document.body || el === document.documentElement) {
+ "development" !== 'production' && warn(
+ "Do not mount Vue to <html> or <body> - mount to normal elements instead."
+ );
+ return this
+ }
- /**
- * 'item' => 'items'
- *
- * @params
- * an array of strings corresponding to
- * the single, double, triple ... forms of the word to
- * be pluralized. When the number to be pluralized
- * exceeds the length of the args, it will use the last
- * entry in the array.
- *
- * e.g. ['single', 'double', 'triple', 'multiple']
- */
-
- pluralize: function pluralize(value) {
- var args = toArray(arguments, 1);
- var length = args.length;
- if (length > 1) {
- var index = value % 10 - 1;
- return index in args ? args[index] : args[length - 1];
+ var options = this.$options;
+ // resolve template/el and convert to render function
+ if (!options.render) {
+ var template = options.template;
+ if (template) {
+ if (typeof template === 'string') {
+ if (template.charAt(0) === '#') {
+ template = idToTemplate(template);
+ }
+ } else if (template.nodeType) {
+ template = template.innerHTML;
} else {
- return args[0] + (value === 1 ? '' : 's');
- }
- },
-
- /**
- * Debounce a handler function.
- *
- * @param {Function} handler
- * @param {Number} delay = 300
- * @return {Function}
- */
-
- debounce: function debounce(handler, delay) {
- if (!handler) return;
- if (!delay) {
- delay = 300;
- }
- return _debounce(handler, delay);
- }
- };
-
- function installGlobalAPI (Vue) {
- /**
- * Vue and every constructor that extends Vue has an
- * associated options object, which can be accessed during
- * compilation steps as `this.constructor.options`.
- *
- * These can be seen as the default options of every
- * Vue instance.
- */
-
- Vue.options = {
- directives: directives,
- elementDirectives: elementDirectives,
- filters: filters,
- transitions: {},
- components: {},
- partials: {},
- replace: true
- };
-
- /**
- * Expose useful internals
- */
-
- Vue.util = util;
- Vue.config = config;
- Vue.set = set;
- Vue['delete'] = del;
- Vue.nextTick = nextTick;
-
- /**
- * The following are exposed for advanced usage / plugins
- */
-
- Vue.compiler = compiler;
- Vue.FragmentFactory = FragmentFactory;
- Vue.internalDirectives = internalDirectives;
- Vue.parsers = {
- path: path,
- text: text,
- template: template,
- directive: directive,
- expression: expression
- };
-
- /**
- * Each instance constructor, including Vue, has a unique
- * cid. This enables us to create wrapped "child
- * constructors" for prototypal inheritance and cache them.
- */
-
- Vue.cid = 0;
- var cid = 1;
-
- /**
- * Class inheritance
- *
- * @param {Object} extendOptions
- */
-
- Vue.extend = function (extendOptions) {
- extendOptions = extendOptions || {};
- var Super = this;
- var isFirstExtend = Super.cid === 0;
- if (isFirstExtend && extendOptions._Ctor) {
- return extendOptions._Ctor;
- }
- var name = extendOptions.name || Super.options.name;
- if ('development' !== 'production') {
- if (!/^[a-zA-Z][\w-]*$/.test(name)) {
- warn('Invalid component name: "' + name + '". Component names ' + 'can only contain alphanumeric characaters and the hyphen.');
- name = null;
+ {
+ warn('invalid template option:' + template, this);
}
+ return this
}
- var Sub = createClass(name || 'VueComponent');
- Sub.prototype = Object.create(Super.prototype);
- Sub.prototype.constructor = Sub;
- Sub.cid = cid++;
- Sub.options = mergeOptions(Super.options, extendOptions);
- Sub['super'] = Super;
- // allow further extension
- Sub.extend = Super.extend;
- // create asset registers, so extended classes
- // can have their private assets too.
- config._assetTypes.forEach(function (type) {
- Sub[type] = Super[type];
- });
- // enable recursive self-lookup
- if (name) {
- Sub.options.components[name] = Sub;
- }
- // cache constructor
- if (isFirstExtend) {
- extendOptions._Ctor = Sub;
- }
- return Sub;
- };
-
- /**
- * A function that returns a sub-class constructor with the
- * given name. This gives us much nicer output when
- * logging instances in the console.
- *
- * @param {String} name
- * @return {Function}
- */
-
- function createClass(name) {
- /* eslint-disable no-new-func */
- return new Function('return function ' + classify(name) + ' (options) { this._init(options) }')();
- /* eslint-enable no-new-func */
+ } else if (el) {
+ template = getOuterHTML(el);
+ }
+ if (template) {
+ var ref = compileToFunctions(template, {
+ warn: warn,
+ shouldDecodeNewlines: shouldDecodeNewlines,
+ delimiters: options.delimiters
+ }, this);
+ var render = ref.render;
+ var staticRenderFns = ref.staticRenderFns;
+ options.render = render;
+ options.staticRenderFns = staticRenderFns;
}
-
- /**
- * Plugin system
- *
- * @param {Object} plugin
- */
-
- Vue.use = function (plugin) {
- /* istanbul ignore if */
- if (plugin.installed) {
- return;
- }
- // additional parameters
- var args = toArray(arguments, 1);
- args.unshift(this);
- if (typeof plugin.install === 'function') {
- plugin.install.apply(plugin, args);
- } else {
- plugin.apply(null, args);
- }
- plugin.installed = true;
- return this;
- };
-
- /**
- * Apply a global mixin by merging it into the default
- * options.
- */
-
- Vue.mixin = function (mixin) {
- Vue.options = mergeOptions(Vue.options, mixin);
- };
-
- /**
- * Create asset registration methods with the following
- * signature:
- *
- * @param {String} id
- * @param {*} definition
- */
-
- config._assetTypes.forEach(function (type) {
- Vue[type] = function (id, definition) {
- if (!definition) {
- return this.options[type + 's'][id];
- } else {
- /* istanbul ignore if */
- if ('development' !== 'production') {
- if (type === 'component' && (commonTagRE.test(id) || reservedTagRE.test(id))) {
- warn('Do not use built-in or reserved HTML elements as component ' + 'id: ' + id);
- }
- }
- if (type === 'component' && isPlainObject(definition)) {
- if (!definition.name) {
- definition.name = id;
- }
- definition = Vue.extend(definition);
- }
- this.options[type + 's'][id] = definition;
- return definition;
- }
- };
- });
-
- // expose internal transition API
- extend(Vue.transition, transition);
}
+ return mount.call(this, el, hydrating)
+};
- installGlobalAPI(Vue);
-
- Vue.version = '1.0.26';
+/**
+ * Get outerHTML of elements, taking care
+ * of SVG elements in IE as well.
+ */
+function getOuterHTML (el) {
+ if (el.outerHTML) {
+ return el.outerHTML
+ } else {
+ var container = document.createElement('div');
+ container.appendChild(el.cloneNode(true));
+ return container.innerHTML
+ }
+}
- // devtools global hook
- /* istanbul ignore next */
- setTimeout(function () {
- if (config.devtools) {
- if (devtools) {
- devtools.emit('init', Vue);
- } else if ('development' !== 'production' && inBrowser && /Chrome\/\d+/.test(window.navigator.userAgent)) {
- console.log('Download the Vue Devtools for a better development experience:\n' + 'https://github.com/vuejs/vue-devtools');
- }
- }
- }, 0);
+Vue$3.compile = compileToFunctions;
- return Vue;
+return Vue$3;
-})); \ No newline at end of file
+})));
diff --git a/vendor/assets/javascripts/vue.min.js b/vendor/assets/javascripts/vue.min.js
index 2c9a8a0e117..f86786dd454 100644
--- a/vendor/assets/javascripts/vue.min.js
+++ b/vendor/assets/javascripts/vue.min.js
@@ -1,9 +1,7 @@
/*!
- * Vue.js v1.0.26
- * (c) 2016 Evan You
+ * Vue.js v2.0.3
+ * (c) 2014-2016 Evan You
* Released under the MIT License.
*/
-!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.Vue=e()}(this,function(){"use strict";function t(e,n,r){if(i(e,n))return void(e[n]=r);if(e._isVue)return void t(e._data,n,r);var s=e.__ob__;if(!s)return void(e[n]=r);if(s.convert(n,r),s.dep.notify(),s.vms)for(var o=s.vms.length;o--;){var a=s.vms[o];a._proxy(n),a._digest()}return r}function e(t,e){if(i(t,e)){delete t[e];var n=t.__ob__;if(!n)return void(t._isVue&&(delete t._data[e],t._digest()));if(n.dep.notify(),n.vms)for(var r=n.vms.length;r--;){var s=n.vms[r];s._unproxy(e),s._digest()}}}function i(t,e){return Oi.call(t,e)}function n(t){return Ti.test(t)}function r(t){var e=(t+"").charCodeAt(0);return 36===e||95===e}function s(t){return null==t?"":t.toString()}function o(t){if("string"!=typeof t)return t;var e=Number(t);return isNaN(e)?t:e}function a(t){return"true"===t?!0:"false"===t?!1:t}function h(t){var e=t.charCodeAt(0),i=t.charCodeAt(t.length-1);return e!==i||34!==e&&39!==e?t:t.slice(1,-1)}function l(t){return t.replace(Ni,c)}function c(t,e){return e?e.toUpperCase():""}function u(t){return t.replace(ji,"$1-$2").toLowerCase()}function f(t){return t.replace(Ei,c)}function p(t,e){return function(i){var n=arguments.length;return n?n>1?t.apply(e,arguments):t.call(e,i):t.call(e)}}function d(t,e){e=e||0;for(var i=t.length-e,n=new Array(i);i--;)n[i]=t[i+e];return n}function v(t,e){for(var i=Object.keys(e),n=i.length;n--;)t[i[n]]=e[i[n]];return t}function m(t){return null!==t&&"object"==typeof t}function g(t){return Si.call(t)===Fi}function _(t,e,i,n){Object.defineProperty(t,e,{value:i,enumerable:!!n,writable:!0,configurable:!0})}function y(t,e){var i,n,r,s,o,a=function h(){var a=Date.now()-s;e>a&&a>=0?i=setTimeout(h,e-a):(i=null,o=t.apply(r,n),i||(r=n=null))};return function(){return r=this,n=arguments,s=Date.now(),i||(i=setTimeout(a,e)),o}}function b(t,e){for(var i=t.length;i--;)if(t[i]===e)return i;return-1}function w(t){var e=function i(){return i.cancelled?void 0:t.apply(this,arguments)};return e.cancel=function(){e.cancelled=!0},e}function C(t,e){return t==e||(m(t)&&m(e)?JSON.stringify(t)===JSON.stringify(e):!1)}function $(t){this.size=0,this.limit=t,this.head=this.tail=void 0,this._keymap=Object.create(null)}function k(){var t,e=en.slice(hn,on).trim();if(e){t={};var i=e.match(vn);t.name=i[0],i.length>1&&(t.args=i.slice(1).map(x))}t&&(nn.filters=nn.filters||[]).push(t),hn=on+1}function x(t){if(mn.test(t))return{value:o(t),dynamic:!1};var e=h(t),i=e===t;return{value:i?t:e,dynamic:i}}function A(t){var e=dn.get(t);if(e)return e;for(en=t,ln=cn=!1,un=fn=pn=0,hn=0,nn={},on=0,an=en.length;an>on;on++)if(sn=rn,rn=en.charCodeAt(on),ln)39===rn&&92!==sn&&(ln=!ln);else if(cn)34===rn&&92!==sn&&(cn=!cn);else if(124===rn&&124!==en.charCodeAt(on+1)&&124!==en.charCodeAt(on-1))null==nn.expression?(hn=on+1,nn.expression=en.slice(0,on).trim()):k();else switch(rn){case 34:cn=!0;break;case 39:ln=!0;break;case 40:pn++;break;case 41:pn--;break;case 91:fn++;break;case 93:fn--;break;case 123:un++;break;case 125:un--}return null==nn.expression?nn.expression=en.slice(0,on).trim():0!==hn&&k(),dn.put(t,nn),nn}function O(t){return t.replace(_n,"\\$&")}function T(){var t=O(An.delimiters[0]),e=O(An.delimiters[1]),i=O(An.unsafeDelimiters[0]),n=O(An.unsafeDelimiters[1]);bn=new RegExp(i+"((?:.|\\n)+?)"+n+"|"+t+"((?:.|\\n)+?)"+e,"g"),wn=new RegExp("^"+i+"((?:.|\\n)+?)"+n+"$"),yn=new $(1e3)}function N(t){yn||T();var e=yn.get(t);if(e)return e;if(!bn.test(t))return null;for(var i,n,r,s,o,a,h=[],l=bn.lastIndex=0;i=bn.exec(t);)n=i.index,n>l&&h.push({value:t.slice(l,n)}),r=wn.test(i[0]),s=r?i[1]:i[2],o=s.charCodeAt(0),a=42===o,s=a?s.slice(1):s,h.push({tag:!0,value:s.trim(),html:r,oneTime:a}),l=n+i[0].length;return l<t.length&&h.push({value:t.slice(l)}),yn.put(t,h),h}function j(t,e){return t.length>1?t.map(function(t){return E(t,e)}).join("+"):E(t[0],e,!0)}function E(t,e,i){return t.tag?t.oneTime&&e?'"'+e.$eval(t.value)+'"':S(t.value,i):'"'+t.value+'"'}function S(t,e){if(Cn.test(t)){var i=A(t);return i.filters?"this._applyFilters("+i.expression+",null,"+JSON.stringify(i.filters)+",false)":"("+t+")"}return e?t:"("+t+")"}function F(t,e,i,n){R(t,1,function(){e.appendChild(t)},i,n)}function D(t,e,i,n){R(t,1,function(){B(t,e)},i,n)}function P(t,e,i){R(t,-1,function(){z(t)},e,i)}function R(t,e,i,n,r){var s=t.__v_trans;if(!s||!s.hooks&&!qi||!n._isCompiled||n.$parent&&!n.$parent._isCompiled)return i(),void(r&&r());var o=e>0?"enter":"leave";s[o](i,r)}function L(t){if("string"==typeof t){t=document.querySelector(t)}return t}function H(t){if(!t)return!1;var e=t.ownerDocument.documentElement,i=t.parentNode;return e===t||e===i||!(!i||1!==i.nodeType||!e.contains(i))}function I(t,e){var i=t.getAttribute(e);return null!==i&&t.removeAttribute(e),i}function M(t,e){var i=I(t,":"+e);return null===i&&(i=I(t,"v-bind:"+e)),i}function V(t,e){return t.hasAttribute(e)||t.hasAttribute(":"+e)||t.hasAttribute("v-bind:"+e)}function B(t,e){e.parentNode.insertBefore(t,e)}function W(t,e){e.nextSibling?B(t,e.nextSibling):e.parentNode.appendChild(t)}function z(t){t.parentNode.removeChild(t)}function U(t,e){e.firstChild?B(t,e.firstChild):e.appendChild(t)}function J(t,e){var i=t.parentNode;i&&i.replaceChild(e,t)}function q(t,e,i,n){t.addEventListener(e,i,n)}function Q(t,e,i){t.removeEventListener(e,i)}function G(t){var e=t.className;return"object"==typeof e&&(e=e.baseVal||""),e}function Z(t,e){Mi&&!/svg$/.test(t.namespaceURI)?t.className=e:t.setAttribute("class",e)}function X(t,e){if(t.classList)t.classList.add(e);else{var i=" "+G(t)+" ";i.indexOf(" "+e+" ")<0&&Z(t,(i+e).trim())}}function Y(t,e){if(t.classList)t.classList.remove(e);else{for(var i=" "+G(t)+" ",n=" "+e+" ";i.indexOf(n)>=0;)i=i.replace(n," ");Z(t,i.trim())}t.className||t.removeAttribute("class")}function K(t,e){var i,n;if(it(t)&&at(t.content)&&(t=t.content),t.hasChildNodes())for(tt(t),n=e?document.createDocumentFragment():document.createElement("div");i=t.firstChild;)n.appendChild(i);return n}function tt(t){for(var e;e=t.firstChild,et(e);)t.removeChild(e);for(;e=t.lastChild,et(e);)t.removeChild(e)}function et(t){return t&&(3===t.nodeType&&!t.data.trim()||8===t.nodeType)}function it(t){return t.tagName&&"template"===t.tagName.toLowerCase()}function nt(t,e){var i=An.debug?document.createComment(t):document.createTextNode(e?" ":"");return i.__v_anchor=!0,i}function rt(t){if(t.hasAttributes())for(var e=t.attributes,i=0,n=e.length;n>i;i++){var r=e[i].name;if(Nn.test(r))return l(r.replace(Nn,""))}}function st(t,e,i){for(var n;t!==e;)n=t.nextSibling,i(t),t=n;i(e)}function ot(t,e,i,n,r){function s(){if(a++,o&&a>=h.length){for(var t=0;t<h.length;t++)n.appendChild(h[t]);r&&r()}}var o=!1,a=0,h=[];st(t,e,function(t){t===e&&(o=!0),h.push(t),P(t,i,s)})}function at(t){return t&&11===t.nodeType}function ht(t){if(t.outerHTML)return t.outerHTML;var e=document.createElement("div");return e.appendChild(t.cloneNode(!0)),e.innerHTML}function lt(t,e){var i=t.tagName.toLowerCase(),n=t.hasAttributes();if(jn.test(i)||En.test(i)){if(n)return ct(t,e)}else{if(gt(e,"components",i))return{id:i};var r=n&&ct(t,e);if(r)return r}}function ct(t,e){var i=t.getAttribute("is");if(null!=i){if(gt(e,"components",i))return t.removeAttribute("is"),{id:i}}else if(i=M(t,"is"),null!=i)return{id:i,dynamic:!0}}function ut(e,n){var r,s,o;for(r in n)s=e[r],o=n[r],i(e,r)?m(s)&&m(o)&&ut(s,o):t(e,r,o);return e}function ft(t,e){var i=Object.create(t||null);return e?v(i,vt(e)):i}function pt(t){if(t.components)for(var e,i=t.components=vt(t.components),n=Object.keys(i),r=0,s=n.length;s>r;r++){var o=n[r];jn.test(o)||En.test(o)||(e=i[o],g(e)&&(i[o]=wi.extend(e)))}}function dt(t){var e,i,n=t.props;if(Di(n))for(t.props={},e=n.length;e--;)i=n[e],"string"==typeof i?t.props[i]=null:i.name&&(t.props[i.name]=i);else if(g(n)){var r=Object.keys(n);for(e=r.length;e--;)i=n[r[e]],"function"==typeof i&&(n[r[e]]={type:i})}}function vt(t){if(Di(t)){for(var e,i={},n=t.length;n--;){e=t[n];var r="function"==typeof e?e.options&&e.options.name||e.id:e.name||e.id;r&&(i[r]=e)}return i}return t}function mt(t,e,n){function r(i){var r=Sn[i]||Fn;o[i]=r(t[i],e[i],n,i)}pt(e),dt(e);var s,o={};if(e["extends"]&&(t="function"==typeof e["extends"]?mt(t,e["extends"].options,n):mt(t,e["extends"],n)),e.mixins)for(var a=0,h=e.mixins.length;h>a;a++){var l=e.mixins[a],c=l.prototype instanceof wi?l.options:l;t=mt(t,c,n)}for(s in t)r(s);for(s in e)i(t,s)||r(s);return o}function gt(t,e,i,n){if("string"==typeof i){var r,s=t[e],o=s[i]||s[r=l(i)]||s[r.charAt(0).toUpperCase()+r.slice(1)];return o}}function _t(){this.id=Dn++,this.subs=[]}function yt(t){Hn=!1,t(),Hn=!0}function bt(t){if(this.value=t,this.dep=new _t,_(t,"__ob__",this),Di(t)){var e=Pi?wt:Ct;e(t,Rn,Ln),this.observeArray(t)}else this.walk(t)}function wt(t,e){t.__proto__=e}function Ct(t,e,i){for(var n=0,r=i.length;r>n;n++){var s=i[n];_(t,s,e[s])}}function $t(t,e){if(t&&"object"==typeof t){var n;return i(t,"__ob__")&&t.__ob__ instanceof bt?n=t.__ob__:Hn&&(Di(t)||g(t))&&Object.isExtensible(t)&&!t._isVue&&(n=new bt(t)),n&&e&&n.addVm(e),n}}function kt(t,e,i){var n=new _t,r=Object.getOwnPropertyDescriptor(t,e);if(!r||r.configurable!==!1){var s=r&&r.get,o=r&&r.set,a=$t(i);Object.defineProperty(t,e,{enumerable:!0,configurable:!0,get:function(){var e=s?s.call(t):i;if(_t.target&&(n.depend(),a&&a.dep.depend(),Di(e)))for(var r,o=0,h=e.length;h>o;o++)r=e[o],r&&r.__ob__&&r.__ob__.dep.depend();return e},set:function(e){var r=s?s.call(t):i;e!==r&&(o?o.call(t,e):i=e,a=$t(e),n.notify())}})}}function xt(t){t.prototype._init=function(t){t=t||{},this.$el=null,this.$parent=t.parent,this.$root=this.$parent?this.$parent.$root:this,this.$children=[],this.$refs={},this.$els={},this._watchers=[],this._directives=[],this._uid=Mn++,this._isVue=!0,this._events={},this._eventsCount={},this._isFragment=!1,this._fragment=this._fragmentStart=this._fragmentEnd=null,this._isCompiled=this._isDestroyed=this._isReady=this._isAttached=this._isBeingDestroyed=this._vForRemoving=!1,this._unlinkFn=null,this._context=t._context||this.$parent,this._scope=t._scope,this._frag=t._frag,this._frag&&this._frag.children.push(this),this.$parent&&this.$parent.$children.push(this),t=this.$options=mt(this.constructor.options,t,this),this._updateRef(),this._data={},this._callHook("init"),this._initState(),this._initEvents(),this._callHook("created"),t.el&&this.$mount(t.el)}}function At(t){if(void 0===t)return"eof";var e=t.charCodeAt(0);switch(e){case 91:case 93:case 46:case 34:case 39:case 48:return t;case 95:case 36:return"ident";case 32:case 9:case 10:case 13:case 160:case 65279:case 8232:case 8233:return"ws"}return e>=97&&122>=e||e>=65&&90>=e?"ident":e>=49&&57>=e?"number":"else"}function Ot(t){var e=t.trim();return"0"===t.charAt(0)&&isNaN(t)?!1:n(e)?h(e):"*"+e}function Tt(t){function e(){var e=t[c+1];return u===Xn&&"'"===e||u===Yn&&'"'===e?(c++,n="\\"+e,p[Bn](),!0):void 0}var i,n,r,s,o,a,h,l=[],c=-1,u=Jn,f=0,p=[];for(p[Wn]=function(){void 0!==r&&(l.push(r),r=void 0)},p[Bn]=function(){void 0===r?r=n:r+=n},p[zn]=function(){p[Bn](),f++},p[Un]=function(){if(f>0)f--,u=Zn,p[Bn]();else{if(f=0,r=Ot(r),r===!1)return!1;p[Wn]()}};null!=u;)if(c++,i=t[c],"\\"!==i||!e()){if(s=At(i),h=er[u],o=h[s]||h["else"]||tr,o===tr)return;if(u=o[0],a=p[o[1]],a&&(n=o[2],n=void 0===n?i:n,a()===!1))return;if(u===Kn)return l.raw=t,l}}function Nt(t){var e=Vn.get(t);return e||(e=Tt(t),e&&Vn.put(t,e)),e}function jt(t,e){return It(e).get(t)}function Et(e,i,n){var r=e;if("string"==typeof i&&(i=Tt(i)),!i||!m(e))return!1;for(var s,o,a=0,h=i.length;h>a;a++)s=e,o=i[a],"*"===o.charAt(0)&&(o=It(o.slice(1)).get.call(r,r)),h-1>a?(e=e[o],m(e)||(e={},t(s,o,e))):Di(e)?e.$set(o,n):o in e?e[o]=n:t(e,o,n);return!0}function St(){}function Ft(t,e){var i=vr.length;return vr[i]=e?t.replace(lr,"\\n"):t,'"'+i+'"'}function Dt(t){var e=t.charAt(0),i=t.slice(1);return sr.test(i)?t:(i=i.indexOf('"')>-1?i.replace(ur,Pt):i,e+"scope."+i)}function Pt(t,e){return vr[e]}function Rt(t){ar.test(t),vr.length=0;var e=t.replace(cr,Ft).replace(hr,"");return e=(" "+e).replace(pr,Dt).replace(ur,Pt),Lt(e)}function Lt(t){try{return new Function("scope","return "+t+";")}catch(e){return St}}function Ht(t){var e=Nt(t);return e?function(t,i){Et(t,e,i)}:void 0}function It(t,e){t=t.trim();var i=nr.get(t);if(i)return e&&!i.set&&(i.set=Ht(i.exp)),i;var n={exp:t};return n.get=Mt(t)&&t.indexOf("[")<0?Lt("scope."+t):Rt(t),e&&(n.set=Ht(t)),nr.put(t,n),n}function Mt(t){return fr.test(t)&&!dr.test(t)&&"Math."!==t.slice(0,5)}function Vt(){gr.length=0,_r.length=0,yr={},br={},wr=!1}function Bt(){for(var t=!0;t;)t=!1,Wt(gr),Wt(_r),gr.length?t=!0:(Li&&An.devtools&&Li.emit("flush"),Vt())}function Wt(t){for(var e=0;e<t.length;e++){var i=t[e],n=i.id;yr[n]=null,i.run()}t.length=0}function zt(t){var e=t.id;if(null==yr[e]){var i=t.user?_r:gr;yr[e]=i.length,i.push(t),wr||(wr=!0,Yi(Bt))}}function Ut(t,e,i,n){n&&v(this,n);var r="function"==typeof e;if(this.vm=t,t._watchers.push(this),this.expression=e,this.cb=i,this.id=++Cr,this.active=!0,this.dirty=this.lazy,this.deps=[],this.newDeps=[],this.depIds=new Ki,this.newDepIds=new Ki,this.prevError=null,r)this.getter=e,this.setter=void 0;else{var s=It(e,this.twoWay);this.getter=s.get,this.setter=s.set}this.value=this.lazy?void 0:this.get(),this.queued=this.shallow=!1}function Jt(t,e){var i=void 0,n=void 0;e||(e=$r,e.clear());var r=Di(t),s=m(t);if((r||s)&&Object.isExtensible(t)){if(t.__ob__){var o=t.__ob__.dep.id;if(e.has(o))return;e.add(o)}if(r)for(i=t.length;i--;)Jt(t[i],e);else if(s)for(n=Object.keys(t),i=n.length;i--;)Jt(t[n[i]],e)}}function qt(t){return it(t)&&at(t.content)}function Qt(t,e){var i=e?t:t.trim(),n=xr.get(i);if(n)return n;var r=document.createDocumentFragment(),s=t.match(Tr),o=Nr.test(t),a=jr.test(t);if(s||o||a){var h=s&&s[1],l=Or[h]||Or.efault,c=l[0],u=l[1],f=l[2],p=document.createElement("div");for(p.innerHTML=u+t+f;c--;)p=p.lastChild;for(var d;d=p.firstChild;)r.appendChild(d)}else r.appendChild(document.createTextNode(t));return e||tt(r),xr.put(i,r),r}function Gt(t){if(qt(t))return Qt(t.innerHTML);if("SCRIPT"===t.tagName)return Qt(t.textContent);for(var e,i=Zt(t),n=document.createDocumentFragment();e=i.firstChild;)n.appendChild(e);return tt(n),n}function Zt(t){if(!t.querySelectorAll)return t.cloneNode();var e,i,n,r=t.cloneNode(!0);if(Er){var s=r;if(qt(t)&&(t=t.content,s=r.content),i=t.querySelectorAll("template"),i.length)for(n=s.querySelectorAll("template"),e=n.length;e--;)n[e].parentNode.replaceChild(Zt(i[e]),n[e])}if(Sr)if("TEXTAREA"===t.tagName)r.value=t.value;else if(i=t.querySelectorAll("textarea"),i.length)for(n=r.querySelectorAll("textarea"),e=n.length;e--;)n[e].value=i[e].value;return r}function Xt(t,e,i){var n,r;return at(t)?(tt(t),e?Zt(t):t):("string"==typeof t?i||"#"!==t.charAt(0)?r=Qt(t,i):(r=Ar.get(t),r||(n=document.getElementById(t.slice(1)),n&&(r=Gt(n),Ar.put(t,r)))):t.nodeType&&(r=Gt(t)),r&&e?Zt(r):r)}function Yt(t,e,i,n,r,s){this.children=[],this.childFrags=[],this.vm=e,this.scope=r,this.inserted=!1,this.parentFrag=s,s&&s.childFrags.push(this),this.unlink=t(e,i,n,r,this);var o=this.single=1===i.childNodes.length&&!i.childNodes[0].__v_anchor;o?(this.node=i.childNodes[0],this.before=Kt,this.remove=te):(this.node=nt("fragment-start"),this.end=nt("fragment-end"),this.frag=i,U(this.node,i),i.appendChild(this.end),this.before=ee,this.remove=ie),this.node.__v_frag=this}function Kt(t,e){this.inserted=!0;var i=e!==!1?D:B;i(this.node,t,this.vm),H(this.node)&&this.callHook(ne)}function te(){this.inserted=!1;var t=H(this.node),e=this;this.beforeRemove(),P(this.node,this.vm,function(){t&&e.callHook(re),e.destroy()})}function ee(t,e){this.inserted=!0;var i=this.vm,n=e!==!1?D:B;st(this.node,this.end,function(e){n(e,t,i)}),H(this.node)&&this.callHook(ne)}function ie(){this.inserted=!1;var t=this,e=H(this.node);this.beforeRemove(),ot(this.node,this.end,this.vm,this.frag,function(){e&&t.callHook(re),t.destroy()})}function ne(t){!t._isAttached&&H(t.$el)&&t._callHook("attached")}function re(t){t._isAttached&&!H(t.$el)&&t._callHook("detached")}function se(t,e){this.vm=t;var i,n="string"==typeof e;n||it(e)&&!e.hasAttribute("v-if")?i=Xt(e,!0):(i=document.createDocumentFragment(),i.appendChild(e)),this.template=i;var r,s=t.constructor.cid;if(s>0){var o=s+(n?e:ht(e));r=Pr.get(o),r||(r=De(i,t.$options,!0),Pr.put(o,r))}else r=De(i,t.$options,!0);this.linker=r}function oe(t,e,i){var n=t.node.previousSibling;if(n){for(t=n.__v_frag;!(t&&t.forId===i&&t.inserted||n===e);){if(n=n.previousSibling,!n)return;t=n.__v_frag}return t}}function ae(t){var e=t.node;if(t.end)for(;!e.__vue__&&e!==t.end&&e.nextSibling;)e=e.nextSibling;return e.__vue__}function he(t){for(var e=-1,i=new Array(Math.floor(t));++e<t;)i[e]=e;return i}function le(t,e,i,n){return n?"$index"===n?t:n.charAt(0).match(/\w/)?jt(i,n):i[n]:e||i}function ce(t,e,i){for(var n,r,s,o=e?[]:null,a=0,h=t.options.length;h>a;a++)if(n=t.options[a],s=i?n.hasAttribute("selected"):n.selected){if(r=n.hasOwnProperty("_value")?n._value:n.value,!e)return r;o.push(r)}return o}function ue(t,e){for(var i=t.length;i--;)if(C(t[i],e))return i;return-1}function fe(t,e){var i=e.map(function(t){var e=t.charCodeAt(0);return e>47&&58>e?parseInt(t,10):1===t.length&&(e=t.toUpperCase().charCodeAt(0),e>64&&91>e)?e:is[t]});return i=[].concat.apply([],i),function(e){return i.indexOf(e.keyCode)>-1?t.call(this,e):void 0}}function pe(t){return function(e){return e.stopPropagation(),t.call(this,e)}}function de(t){return function(e){return e.preventDefault(),t.call(this,e)}}function ve(t){return function(e){return e.target===e.currentTarget?t.call(this,e):void 0}}function me(t){if(as[t])return as[t];var e=ge(t);return as[t]=as[e]=e,e}function ge(t){t=u(t);var e=l(t),i=e.charAt(0).toUpperCase()+e.slice(1);hs||(hs=document.createElement("div"));var n,r=rs.length;if("filter"!==e&&e in hs.style)return{kebab:t,camel:e};for(;r--;)if(n=ss[r]+i,n in hs.style)return{kebab:rs[r]+t,camel:n}}function _e(t){var e=[];if(Di(t))for(var i=0,n=t.length;n>i;i++){var r=t[i];if(r)if("string"==typeof r)e.push(r);else for(var s in r)r[s]&&e.push(s)}else if(m(t))for(var o in t)t[o]&&e.push(o);return e}function ye(t,e,i){if(e=e.trim(),-1===e.indexOf(" "))return void i(t,e);for(var n=e.split(/\s+/),r=0,s=n.length;s>r;r++)i(t,n[r])}function be(t,e,i){function n(){++s>=r?i():t[s].call(e,n)}var r=t.length,s=0;t[0].call(e,n)}function we(t,e,i){for(var r,s,o,a,h,c,f,p=[],d=Object.keys(e),v=d.length;v--;)s=d[v],r=e[s]||ks,h=l(s),xs.test(h)&&(f={name:s,path:h,options:r,mode:$s.ONE_WAY,raw:null},o=u(s),null===(a=M(t,o))&&(null!==(a=M(t,o+".sync"))?f.mode=$s.TWO_WAY:null!==(a=M(t,o+".once"))&&(f.mode=$s.ONE_TIME)),null!==a?(f.raw=a,c=A(a),a=c.expression,f.filters=c.filters,n(a)&&!c.filters?f.optimizedLiteral=!0:f.dynamic=!0,f.parentPath=a):null!==(a=I(t,o))&&(f.raw=a),p.push(f));return Ce(p)}function Ce(t){return function(e,n){e._props={};for(var r,s,l,c,f,p=e.$options.propsData,d=t.length;d--;)if(r=t[d],f=r.raw,s=r.path,l=r.options,e._props[s]=r,p&&i(p,s)&&ke(e,r,p[s]),null===f)ke(e,r,void 0);else if(r.dynamic)r.mode===$s.ONE_TIME?(c=(n||e._context||e).$get(r.parentPath),ke(e,r,c)):e._context?e._bindDir({name:"prop",def:Os,prop:r},null,null,n):ke(e,r,e.$get(r.parentPath));else if(r.optimizedLiteral){var v=h(f);c=v===f?a(o(f)):v,ke(e,r,c)}else c=l.type!==Boolean||""!==f&&f!==u(r.name)?f:!0,ke(e,r,c)}}function $e(t,e,i,n){var r=e.dynamic&&Mt(e.parentPath),s=i;void 0===s&&(s=Ae(t,e)),s=Te(e,s,t);var o=s!==i;Oe(e,s,t)||(s=void 0),r&&!o?yt(function(){n(s)}):n(s)}function ke(t,e,i){$e(t,e,i,function(i){kt(t,e.path,i)})}function xe(t,e,i){$e(t,e,i,function(i){t[e.path]=i})}function Ae(t,e){var n=e.options;if(!i(n,"default"))return n.type===Boolean?!1:void 0;var r=n["default"];return m(r),"function"==typeof r&&n.type!==Function?r.call(t):r}function Oe(t,e,i){if(!t.options.required&&(null===t.raw||null==e))return!0;var n=t.options,r=n.type,s=!r,o=[];if(r){Di(r)||(r=[r]);for(var a=0;a<r.length&&!s;a++){var h=Ne(e,r[a]);o.push(h.expectedType),s=h.valid}}if(!s)return!1;var l=n.validator;return!l||l(e)}function Te(t,e,i){var n=t.options.coerce;return n&&"function"==typeof n?n(e):e}function Ne(t,e){var i,n;return e===String?(n="string",i=typeof t===n):e===Number?(n="number",i=typeof t===n):e===Boolean?(n="boolean",i=typeof t===n):e===Function?(n="function",i=typeof t===n):e===Object?(n="object",i=g(t)):e===Array?(n="array",i=Di(t)):i=t instanceof e,{valid:i,expectedType:n}}function je(t){Ts.push(t),Ns||(Ns=!0,Yi(Ee))}function Ee(){for(var t=document.documentElement.offsetHeight,e=0;e<Ts.length;e++)Ts[e]();return Ts=[],Ns=!1,t}function Se(t,e,i,n){this.id=e,this.el=t,this.enterClass=i&&i.enterClass||e+"-enter",this.leaveClass=i&&i.leaveClass||e+"-leave",this.hooks=i,this.vm=n,this.pendingCssEvent=this.pendingCssCb=this.cancel=this.pendingJsCb=this.op=this.cb=null,this.justEntered=!1,this.entered=this.left=!1,this.typeCache={},this.type=i&&i.type;var r=this;["enterNextTick","enterDone","leaveNextTick","leaveDone"].forEach(function(t){r[t]=p(r[t],r)})}function Fe(t){if(/svg$/.test(t.namespaceURI)){var e=t.getBoundingClientRect();return!(e.width||e.height)}return!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)}function De(t,e,i){var n=i||!e._asComponent?Ve(t,e):null,r=n&&n.terminal||ri(t)||!t.hasChildNodes()?null:qe(t.childNodes,e);return function(t,e,i,s,o){var a=d(e.childNodes),h=Pe(function(){n&&n(t,e,i,s,o),r&&r(t,a,i,s,o)},t);return Le(t,h)}}function Pe(t,e){e._directives=[];var i=e._directives.length;t();var n=e._directives.slice(i);n.sort(Re);for(var r=0,s=n.length;s>r;r++)n[r]._bind();return n}function Re(t,e){return t=t.descriptor.def.priority||zs,e=e.descriptor.def.priority||zs,t>e?-1:t===e?0:1}function Le(t,e,i,n){function r(r){He(t,e,r),i&&n&&He(i,n)}return r.dirs=e,r}function He(t,e,i){for(var n=e.length;n--;)e[n]._teardown()}function Ie(t,e,i,n){var r=we(e,i,t),s=Pe(function(){r(t,n)},t);return Le(t,s)}function Me(t,e,i){var n,r,s=e._containerAttrs,o=e._replacerAttrs;return 11!==t.nodeType&&(e._asComponent?(s&&i&&(n=ti(s,i)),o&&(r=ti(o,e))):r=ti(t.attributes,e)),e._containerAttrs=e._replacerAttrs=null,function(t,e,i){var s,o=t._context;o&&n&&(s=Pe(function(){n(o,e,null,i)},o));var a=Pe(function(){r&&r(t,e)},t);return Le(t,a,o,s)}}function Ve(t,e){var i=t.nodeType;return 1!==i||ri(t)?3===i&&t.data.trim()?We(t,e):null:Be(t,e)}function Be(t,e){if("TEXTAREA"===t.tagName){var i=N(t.value);i&&(t.setAttribute(":value",j(i)),t.value="")}var n,r=t.hasAttributes(),s=r&&d(t.attributes);return r&&(n=Xe(t,s,e)),n||(n=Ge(t,e)),n||(n=Ze(t,e)),!n&&r&&(n=ti(s,e)),n}function We(t,e){if(t._skip)return ze;var i=N(t.wholeText);if(!i)return null;for(var n=t.nextSibling;n&&3===n.nodeType;)n._skip=!0,n=n.nextSibling;for(var r,s,o=document.createDocumentFragment(),a=0,h=i.length;h>a;a++)s=i[a],r=s.tag?Ue(s,e):document.createTextNode(s.value),o.appendChild(r);return Je(i,o,e)}function ze(t,e){z(e)}function Ue(t,e){function i(e){if(!t.descriptor){var i=A(t.value);t.descriptor={name:e,def:bs[e],expression:i.expression,filters:i.filters}}}var n;return t.oneTime?n=document.createTextNode(t.value):t.html?(n=document.createComment("v-html"),i("html")):(n=document.createTextNode(" "),i("text")),n}function Je(t,e){return function(i,n,r,o){for(var a,h,l,c=e.cloneNode(!0),u=d(c.childNodes),f=0,p=t.length;p>f;f++)a=t[f],h=a.value,a.tag&&(l=u[f],a.oneTime?(h=(o||i).$eval(h),a.html?J(l,Xt(h,!0)):l.data=s(h)):i._bindDir(a.descriptor,l,r,o));J(n,c)}}function qe(t,e){for(var i,n,r,s=[],o=0,a=t.length;a>o;o++)r=t[o],i=Ve(r,e),n=i&&i.terminal||"SCRIPT"===r.tagName||!r.hasChildNodes()?null:qe(r.childNodes,e),s.push(i,n);return s.length?Qe(s):null}function Qe(t){return function(e,i,n,r,s){for(var o,a,h,l=0,c=0,u=t.length;u>l;c++){o=i[c],a=t[l++],h=t[l++];var f=d(o.childNodes);a&&a(e,o,n,r,s),h&&h(e,f,n,r,s)}}}function Ge(t,e){var i=t.tagName.toLowerCase();if(!jn.test(i)){var n=gt(e,"elementDirectives",i);return n?Ke(t,i,"",e,n):void 0}}function Ze(t,e){var i=lt(t,e);if(i){var n=rt(t),r={name:"component",ref:n,expression:i.id,def:Hs.component,modifiers:{literal:!i.dynamic}},s=function(t,e,i,s,o){n&&kt((s||t).$refs,n,null),t._bindDir(r,e,i,s,o)};return s.terminal=!0,s}}function Xe(t,e,i){if(null!==I(t,"v-pre"))return Ye;if(t.hasAttribute("v-else")){var n=t.previousElementSibling;if(n&&n.hasAttribute("v-if"))return Ye}for(var r,s,o,a,h,l,c,u,f,p,d=0,v=e.length;v>d;d++)r=e[d],s=r.name.replace(Bs,""),(h=s.match(Vs))&&(f=gt(i,"directives",h[1]),f&&f.terminal&&(!p||(f.priority||Us)>p.priority)&&(p=f,c=r.name,a=ei(r.name),o=r.value,l=h[1],u=h[2]));return p?Ke(t,l,o,i,p,c,u,a):void 0}function Ye(){}function Ke(t,e,i,n,r,s,o,a){var h=A(i),l={name:e,arg:o,expression:h.expression,filters:h.filters,raw:i,attr:s,modifiers:a,def:r};"for"!==e&&"router-view"!==e||(l.ref=rt(t));var c=function(t,e,i,n,r){l.ref&&kt((n||t).$refs,l.ref,null),t._bindDir(l,e,i,n,r)};return c.terminal=!0,c}function ti(t,e){function i(t,e,i){var n=i&&ni(i),r=!n&&A(s);v.push({name:t,attr:o,raw:a,def:e,arg:l,modifiers:c,expression:r&&r.expression,filters:r&&r.filters,interp:i,hasOneTime:n})}for(var n,r,s,o,a,h,l,c,u,f,p,d=t.length,v=[];d--;)if(n=t[d],r=o=n.name,s=a=n.value,f=N(s),l=null,c=ei(r),r=r.replace(Bs,""),f)s=j(f),l=r,i("bind",bs.bind,f);else if(Ws.test(r))c.literal=!Is.test(r),i("transition",Hs.transition);else if(Ms.test(r))l=r.replace(Ms,""),i("on",bs.on);else if(Is.test(r))h=r.replace(Is,""),"style"===h||"class"===h?i(h,Hs[h]):(l=h,i("bind",bs.bind));else if(p=r.match(Vs)){if(h=p[1],l=p[2],"else"===h)continue;u=gt(e,"directives",h,!0),u&&i(h,u)}return v.length?ii(v):void 0}function ei(t){var e=Object.create(null),i=t.match(Bs);if(i)for(var n=i.length;n--;)e[i[n].slice(1)]=!0;return e}function ii(t){return function(e,i,n,r,s){for(var o=t.length;o--;)e._bindDir(t[o],i,n,r,s)}}function ni(t){for(var e=t.length;e--;)if(t[e].oneTime)return!0}function ri(t){return"SCRIPT"===t.tagName&&(!t.hasAttribute("type")||"text/javascript"===t.getAttribute("type"))}function si(t,e){return e&&(e._containerAttrs=ai(t)),it(t)&&(t=Xt(t)),e&&(e._asComponent&&!e.template&&(e.template="<slot></slot>"),e.template&&(e._content=K(t),t=oi(t,e))),at(t)&&(U(nt("v-start",!0),t),t.appendChild(nt("v-end",!0))),t}function oi(t,e){var i=e.template,n=Xt(i,!0);if(n){var r=n.firstChild,s=r.tagName&&r.tagName.toLowerCase();return e.replace?(t===document.body,n.childNodes.length>1||1!==r.nodeType||"component"===s||gt(e,"components",s)||V(r,"is")||gt(e,"elementDirectives",s)||r.hasAttribute("v-for")||r.hasAttribute("v-if")?n:(e._replacerAttrs=ai(r),hi(t,r),r)):(t.appendChild(n),t)}}function ai(t){return 1===t.nodeType&&t.hasAttributes()?d(t.attributes):void 0}function hi(t,e){for(var i,n,r=t.attributes,s=r.length;s--;)i=r[s].name,n=r[s].value,e.hasAttribute(i)||Js.test(i)?"class"===i&&!N(n)&&(n=n.trim())&&n.split(/\s+/).forEach(function(t){X(e,t)}):e.setAttribute(i,n)}function li(t,e){if(e){for(var i,n,r=t._slotContents=Object.create(null),s=0,o=e.children.length;o>s;s++)i=e.children[s],(n=i.getAttribute("slot"))&&(r[n]||(r[n]=[])).push(i);for(n in r)r[n]=ci(r[n],e);if(e.hasChildNodes()){var a=e.childNodes;if(1===a.length&&3===a[0].nodeType&&!a[0].data.trim())return;r["default"]=ci(e.childNodes,e)}}}function ci(t,e){var i=document.createDocumentFragment();t=d(t);for(var n=0,r=t.length;r>n;n++){var s=t[n];!it(s)||s.hasAttribute("v-if")||s.hasAttribute("v-for")||(e.removeChild(s),s=Xt(s,!0)),i.appendChild(s)}return i}function ui(t){function e(){}function n(t,e){var i=new Ut(e,t,null,{lazy:!0});return function(){return i.dirty&&i.evaluate(),_t.target&&i.depend(),i.value}}Object.defineProperty(t.prototype,"$data",{get:function(){return this._data},set:function(t){t!==this._data&&this._setData(t)}}),t.prototype._initState=function(){this._initProps(),this._initMeta(),this._initMethods(),this._initData(),this._initComputed()},t.prototype._initProps=function(){var t=this.$options,e=t.el,i=t.props;e=t.el=L(e),this._propsUnlinkFn=e&&1===e.nodeType&&i?Ie(this,e,i,this._scope):null},t.prototype._initData=function(){var t=this.$options.data,e=this._data=t?t():{};g(e)||(e={});var n,r,s=this._props,o=Object.keys(e);for(n=o.length;n--;)r=o[n],s&&i(s,r)||this._proxy(r);$t(e,this)},t.prototype._setData=function(t){t=t||{};var e=this._data;this._data=t;var n,r,s;for(n=Object.keys(e),s=n.length;s--;)r=n[s],r in t||this._unproxy(r);for(n=Object.keys(t),s=n.length;s--;)r=n[s],i(this,r)||this._proxy(r);e.__ob__.removeVm(this),$t(t,this),this._digest()},t.prototype._proxy=function(t){if(!r(t)){var e=this;Object.defineProperty(e,t,{configurable:!0,enumerable:!0,get:function(){return e._data[t]},set:function(i){e._data[t]=i}})}},t.prototype._unproxy=function(t){r(t)||delete this[t]},t.prototype._digest=function(){for(var t=0,e=this._watchers.length;e>t;t++)this._watchers[t].update(!0)},t.prototype._initComputed=function(){var t=this.$options.computed;if(t)for(var i in t){var r=t[i],s={enumerable:!0,configurable:!0};"function"==typeof r?(s.get=n(r,this),s.set=e):(s.get=r.get?r.cache!==!1?n(r.get,this):p(r.get,this):e,s.set=r.set?p(r.set,this):e),Object.defineProperty(this,i,s)}},t.prototype._initMethods=function(){var t=this.$options.methods;if(t)for(var e in t)this[e]=p(t[e],this)},t.prototype._initMeta=function(){var t=this.$options._meta;if(t)for(var e in t)kt(this,e,t[e])}}function fi(t){function e(t,e){for(var i,n,r,s=e.attributes,o=0,a=s.length;a>o;o++)i=s[o].name,Qs.test(i)&&(i=i.replace(Qs,""),n=s[o].value,Mt(n)&&(n+=".apply(this, $arguments)"),r=(t._scope||t._context).$eval(n,!0),r._fromParent=!0,t.$on(i.replace(Qs),r))}function i(t,e,i){if(i){var r,s,o,a;for(s in i)if(r=i[s],Di(r))for(o=0,a=r.length;a>o;o++)n(t,e,s,r[o]);else n(t,e,s,r)}}function n(t,e,i,r,s){var o=typeof r;if("function"===o)t[e](i,r,s);else if("string"===o){var a=t.$options.methods,h=a&&a[r];h&&t[e](i,h,s)}else r&&"object"===o&&n(t,e,i,r.handler,r)}function r(){this._isAttached||(this._isAttached=!0,this.$children.forEach(s))}function s(t){!t._isAttached&&H(t.$el)&&t._callHook("attached")}function o(){this._isAttached&&(this._isAttached=!1,this.$children.forEach(a))}function a(t){t._isAttached&&!H(t.$el)&&t._callHook("detached")}t.prototype._initEvents=function(){var t=this.$options;t._asComponent&&e(this,t.el),i(this,"$on",t.events),i(this,"$watch",t.watch)},t.prototype._initDOMHooks=function(){this.$on("hook:attached",r),this.$on("hook:detached",o)},t.prototype._callHook=function(t){this.$emit("pre-hook:"+t);var e=this.$options[t];if(e)for(var i=0,n=e.length;n>i;i++)e[i].call(this);this.$emit("hook:"+t)}}function pi(){}function di(t,e,i,n,r,s){this.vm=e,this.el=i,this.descriptor=t,this.name=t.name,this.expression=t.expression,this.arg=t.arg,this.modifiers=t.modifiers,this.filters=t.filters,this.literal=this.modifiers&&this.modifiers.literal,this._locked=!1,this._bound=!1,this._listeners=null,this._host=n,this._scope=r,this._frag=s}function vi(t){t.prototype._updateRef=function(t){var e=this.$options._ref;if(e){var i=(this._scope||this._context).$refs;t?i[e]===this&&(i[e]=null):i[e]=this}},t.prototype._compile=function(t){var e=this.$options,i=t;if(t=si(t,e),this._initElement(t),1!==t.nodeType||null===I(t,"v-pre")){var n=this._context&&this._context.$options,r=Me(t,e,n);li(this,e._content);var s,o=this.constructor;e._linkerCachable&&(s=o.linker,s||(s=o.linker=De(t,e)));var a=r(this,t,this._scope),h=s?s(this,t):De(t,e)(this,t);this._unlinkFn=function(){a(),h(!0)},e.replace&&J(i,t),this._isCompiled=!0,this._callHook("compiled")}},t.prototype._initElement=function(t){at(t)?(this._isFragment=!0,this.$el=this._fragmentStart=t.firstChild,this._fragmentEnd=t.lastChild,3===this._fragmentStart.nodeType&&(this._fragmentStart.data=this._fragmentEnd.data=""),this._fragment=t):this.$el=t,this.$el.__vue__=this,this._callHook("beforeCompile")},t.prototype._bindDir=function(t,e,i,n,r){this._directives.push(new di(t,this,e,i,n,r))},t.prototype._destroy=function(t,e){if(this._isBeingDestroyed)return void(e||this._cleanup());var i,n,r=this,s=function(){!i||n||e||r._cleanup()};t&&this.$el&&(n=!0,this.$remove(function(){
-n=!1,s()})),this._callHook("beforeDestroy"),this._isBeingDestroyed=!0;var o,a=this.$parent;for(a&&!a._isBeingDestroyed&&(a.$children.$remove(this),this._updateRef(!0)),o=this.$children.length;o--;)this.$children[o].$destroy();for(this._propsUnlinkFn&&this._propsUnlinkFn(),this._unlinkFn&&this._unlinkFn(),o=this._watchers.length;o--;)this._watchers[o].teardown();this.$el&&(this.$el.__vue__=null),i=!0,s()},t.prototype._cleanup=function(){this._isDestroyed||(this._frag&&this._frag.children.$remove(this),this._data&&this._data.__ob__&&this._data.__ob__.removeVm(this),this.$el=this.$parent=this.$root=this.$children=this._watchers=this._context=this._scope=this._directives=null,this._isDestroyed=!0,this._callHook("destroyed"),this.$off())}}function mi(t){t.prototype._applyFilters=function(t,e,i,n){var r,s,o,a,h,l,c,u,f;for(l=0,c=i.length;c>l;l++)if(r=i[n?c-l-1:l],s=gt(this.$options,"filters",r.name,!0),s&&(s=n?s.write:s.read||s,"function"==typeof s)){if(o=n?[t,e]:[t],h=n?2:1,r.args)for(u=0,f=r.args.length;f>u;u++)a=r.args[u],o[u+h]=a.dynamic?this.$get(a.value):a.value;t=s.apply(this,o)}return t},t.prototype._resolveComponent=function(e,i){var n;if(n="function"==typeof e?e:gt(this.$options,"components",e,!0))if(n.options)i(n);else if(n.resolved)i(n.resolved);else if(n.requested)n.pendingCallbacks.push(i);else{n.requested=!0;var r=n.pendingCallbacks=[i];n.call(this,function(e){g(e)&&(e=t.extend(e)),n.resolved=e;for(var i=0,s=r.length;s>i;i++)r[i](e)},function(t){})}}}function gi(t){function i(t){return JSON.parse(JSON.stringify(t))}t.prototype.$get=function(t,e){var i=It(t);if(i){if(e){var n=this;return function(){n.$arguments=d(arguments);var t=i.get.call(n,n);return n.$arguments=null,t}}try{return i.get.call(this,this)}catch(r){}}},t.prototype.$set=function(t,e){var i=It(t,!0);i&&i.set&&i.set.call(this,this,e)},t.prototype.$delete=function(t){e(this._data,t)},t.prototype.$watch=function(t,e,i){var n,r=this;"string"==typeof t&&(n=A(t),t=n.expression);var s=new Ut(r,t,e,{deep:i&&i.deep,sync:i&&i.sync,filters:n&&n.filters,user:!i||i.user!==!1});return i&&i.immediate&&e.call(r,s.value),function(){s.teardown()}},t.prototype.$eval=function(t,e){if(Gs.test(t)){var i=A(t),n=this.$get(i.expression,e);return i.filters?this._applyFilters(n,null,i.filters):n}return this.$get(t,e)},t.prototype.$interpolate=function(t){var e=N(t),i=this;return e?1===e.length?i.$eval(e[0].value)+"":e.map(function(t){return t.tag?i.$eval(t.value):t.value}).join(""):t},t.prototype.$log=function(t){var e=t?jt(this._data,t):this._data;if(e&&(e=i(e)),!t){var n;for(n in this.$options.computed)e[n]=i(this[n]);if(this._props)for(n in this._props)e[n]=i(this[n])}console.log(e)}}function _i(t){function e(t,e,n,r,s,o){e=i(e);var a=!H(e),h=r===!1||a?s:o,l=!a&&!t._isAttached&&!H(t.$el);return t._isFragment?(st(t._fragmentStart,t._fragmentEnd,function(i){h(i,e,t)}),n&&n()):h(t.$el,e,t,n),l&&t._callHook("attached"),t}function i(t){return"string"==typeof t?document.querySelector(t):t}function n(t,e,i,n){e.appendChild(t),n&&n()}function r(t,e,i,n){B(t,e),n&&n()}function s(t,e,i){z(t),i&&i()}t.prototype.$nextTick=function(t){Yi(t,this)},t.prototype.$appendTo=function(t,i,r){return e(this,t,i,r,n,F)},t.prototype.$prependTo=function(t,e,n){return t=i(t),t.hasChildNodes()?this.$before(t.firstChild,e,n):this.$appendTo(t,e,n),this},t.prototype.$before=function(t,i,n){return e(this,t,i,n,r,D)},t.prototype.$after=function(t,e,n){return t=i(t),t.nextSibling?this.$before(t.nextSibling,e,n):this.$appendTo(t.parentNode,e,n),this},t.prototype.$remove=function(t,e){if(!this.$el.parentNode)return t&&t();var i=this._isAttached&&H(this.$el);i||(e=!1);var n=this,r=function(){i&&n._callHook("detached"),t&&t()};if(this._isFragment)ot(this._fragmentStart,this._fragmentEnd,this,this._fragment,r);else{var o=e===!1?s:P;o(this.$el,this,r)}return this}}function yi(t){function e(t,e,n){var r=t.$parent;if(r&&n&&!i.test(e))for(;r;)r._eventsCount[e]=(r._eventsCount[e]||0)+n,r=r.$parent}t.prototype.$on=function(t,i){return(this._events[t]||(this._events[t]=[])).push(i),e(this,t,1),this},t.prototype.$once=function(t,e){function i(){n.$off(t,i),e.apply(this,arguments)}var n=this;return i.fn=e,this.$on(t,i),this},t.prototype.$off=function(t,i){var n;if(!arguments.length){if(this.$parent)for(t in this._events)n=this._events[t],n&&e(this,t,-n.length);return this._events={},this}if(n=this._events[t],!n)return this;if(1===arguments.length)return e(this,t,-n.length),this._events[t]=null,this;for(var r,s=n.length;s--;)if(r=n[s],r===i||r.fn===i){e(this,t,-1),n.splice(s,1);break}return this},t.prototype.$emit=function(t){var e="string"==typeof t;t=e?t:t.name;var i=this._events[t],n=e||!i;if(i){i=i.length>1?d(i):i;var r=e&&i.some(function(t){return t._fromParent});r&&(n=!1);for(var s=d(arguments,1),o=0,a=i.length;a>o;o++){var h=i[o],l=h.apply(this,s);l!==!0||r&&!h._fromParent||(n=!0)}}return n},t.prototype.$broadcast=function(t){var e="string"==typeof t;if(t=e?t:t.name,this._eventsCount[t]){var i=this.$children,n=d(arguments);e&&(n[0]={name:t,source:this});for(var r=0,s=i.length;s>r;r++){var o=i[r],a=o.$emit.apply(o,n);a&&o.$broadcast.apply(o,n)}return this}},t.prototype.$dispatch=function(t){var e=this.$emit.apply(this,arguments);if(e){var i=this.$parent,n=d(arguments);for(n[0]={name:t,source:this};i;)e=i.$emit.apply(i,n),i=e?i.$parent:null;return this}};var i=/^hook:/}function bi(t){function e(){this._isAttached=!0,this._isReady=!0,this._callHook("ready")}t.prototype.$mount=function(t){return this._isCompiled?void 0:(t=L(t),t||(t=document.createElement("div")),this._compile(t),this._initDOMHooks(),H(this.$el)?(this._callHook("attached"),e.call(this)):this.$once("hook:attached",e),this)},t.prototype.$destroy=function(t,e){this._destroy(t,e)},t.prototype.$compile=function(t,e,i,n){return De(t,this.$options,!0)(this,t,e,i,n)}}function wi(t){this._init(t)}function Ci(t,e,i){return i=i?parseInt(i,10):0,e=o(e),"number"==typeof e?t.slice(i,i+e):t}function $i(t,e,i){if(t=Ks(t),null==e)return t;if("function"==typeof e)return t.filter(e);e=(""+e).toLowerCase();for(var n,r,s,o,a="in"===i?3:2,h=Array.prototype.concat.apply([],d(arguments,a)),l=[],c=0,u=t.length;u>c;c++)if(n=t[c],s=n&&n.$value||n,o=h.length){for(;o--;)if(r=h[o],"$key"===r&&xi(n.$key,e)||xi(jt(s,r),e)){l.push(n);break}}else xi(n,e)&&l.push(n);return l}function ki(t){function e(t,e,i){var r=n[i];return r&&("$key"!==r&&(m(t)&&"$value"in t&&(t=t.$value),m(e)&&"$value"in e&&(e=e.$value)),t=m(t)?jt(t,r):t,e=m(e)?jt(e,r):e),t===e?0:t>e?s:-s}var i=null,n=void 0;t=Ks(t);var r=d(arguments,1),s=r[r.length-1];"number"==typeof s?(s=0>s?-1:1,r=r.length>1?r.slice(0,-1):r):s=1;var o=r[0];return o?("function"==typeof o?i=function(t,e){return o(t,e)*s}:(n=Array.prototype.concat.apply([],r),i=function(t,r,s){return s=s||0,s>=n.length-1?e(t,r,s):e(t,r,s)||i(t,r,s+1)}),t.slice().sort(i)):t}function xi(t,e){var i;if(g(t)){var n=Object.keys(t);for(i=n.length;i--;)if(xi(t[n[i]],e))return!0}else if(Di(t)){for(i=t.length;i--;)if(xi(t[i],e))return!0}else if(null!=t)return t.toString().toLowerCase().indexOf(e)>-1}function Ai(i){function n(t){return new Function("return function "+f(t)+" (options) { this._init(options) }")()}i.options={directives:bs,elementDirectives:Ys,filters:eo,transitions:{},components:{},partials:{},replace:!0},i.util=In,i.config=An,i.set=t,i["delete"]=e,i.nextTick=Yi,i.compiler=qs,i.FragmentFactory=se,i.internalDirectives=Hs,i.parsers={path:ir,text:$n,template:Fr,directive:gn,expression:mr},i.cid=0;var r=1;i.extend=function(t){t=t||{};var e=this,i=0===e.cid;if(i&&t._Ctor)return t._Ctor;var s=t.name||e.options.name,o=n(s||"VueComponent");return o.prototype=Object.create(e.prototype),o.prototype.constructor=o,o.cid=r++,o.options=mt(e.options,t),o["super"]=e,o.extend=e.extend,An._assetTypes.forEach(function(t){o[t]=e[t]}),s&&(o.options.components[s]=o),i&&(t._Ctor=o),o},i.use=function(t){if(!t.installed){var e=d(arguments,1);return e.unshift(this),"function"==typeof t.install?t.install.apply(t,e):t.apply(null,e),t.installed=!0,this}},i.mixin=function(t){i.options=mt(i.options,t)},An._assetTypes.forEach(function(t){i[t]=function(e,n){return n?("component"===t&&g(n)&&(n.name||(n.name=e),n=i.extend(n)),this.options[t+"s"][e]=n,n):this.options[t+"s"][e]}}),v(i.transition,Tn)}var Oi=Object.prototype.hasOwnProperty,Ti=/^\s?(true|false|-?[\d\.]+|'[^']*'|"[^"]*")\s?$/,Ni=/-(\w)/g,ji=/([a-z\d])([A-Z])/g,Ei=/(?:^|[-_\/])(\w)/g,Si=Object.prototype.toString,Fi="[object Object]",Di=Array.isArray,Pi="__proto__"in{},Ri="undefined"!=typeof window&&"[object Object]"!==Object.prototype.toString.call(window),Li=Ri&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__,Hi=Ri&&window.navigator.userAgent.toLowerCase(),Ii=Hi&&Hi.indexOf("trident")>0,Mi=Hi&&Hi.indexOf("msie 9.0")>0,Vi=Hi&&Hi.indexOf("android")>0,Bi=Hi&&/(iphone|ipad|ipod|ios)/i.test(Hi),Wi=Bi&&Hi.match(/os ([\d_]+)/),zi=Wi&&Wi[1].split("_"),Ui=zi&&Number(zi[0])>=9&&Number(zi[1])>=3&&!window.indexedDB,Ji=void 0,qi=void 0,Qi=void 0,Gi=void 0;if(Ri&&!Mi){var Zi=void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend,Xi=void 0===window.onanimationend&&void 0!==window.onwebkitanimationend;Ji=Zi?"WebkitTransition":"transition",qi=Zi?"webkitTransitionEnd":"transitionend",Qi=Xi?"WebkitAnimation":"animation",Gi=Xi?"webkitAnimationEnd":"animationend"}var Yi=function(){function t(){n=!1;var t=i.slice(0);i=[];for(var e=0;e<t.length;e++)t[e]()}var e,i=[],n=!1;if("undefined"==typeof MutationObserver||Ui){var r=Ri?window:"undefined"!=typeof global?global:{};e=r.setImmediate||setTimeout}else{var s=1,o=new MutationObserver(t),a=document.createTextNode(s);o.observe(a,{characterData:!0}),e=function(){s=(s+1)%2,a.data=s}}return function(r,s){var o=s?function(){r.call(s)}:r;i.push(o),n||(n=!0,e(t,0))}}(),Ki=void 0;"undefined"!=typeof Set&&Set.toString().match(/native code/)?Ki=Set:(Ki=function(){this.set=Object.create(null)},Ki.prototype.has=function(t){return void 0!==this.set[t]},Ki.prototype.add=function(t){this.set[t]=1},Ki.prototype.clear=function(){this.set=Object.create(null)});var tn=$.prototype;tn.put=function(t,e){var i,n=this.get(t,!0);return n||(this.size===this.limit&&(i=this.shift()),n={key:t},this._keymap[t]=n,this.tail?(this.tail.newer=n,n.older=this.tail):this.head=n,this.tail=n,this.size++),n.value=e,i},tn.shift=function(){var t=this.head;return t&&(this.head=this.head.newer,this.head.older=void 0,t.newer=t.older=void 0,this._keymap[t.key]=void 0,this.size--),t},tn.get=function(t,e){var i=this._keymap[t];if(void 0!==i)return i===this.tail?e?i:i.value:(i.newer&&(i===this.head&&(this.head=i.newer),i.newer.older=i.older),i.older&&(i.older.newer=i.newer),i.newer=void 0,i.older=this.tail,this.tail&&(this.tail.newer=i),this.tail=i,e?i:i.value)};var en,nn,rn,sn,on,an,hn,ln,cn,un,fn,pn,dn=new $(1e3),vn=/[^\s'"]+|'[^']*'|"[^"]*"/g,mn=/^in$|^-?\d+/,gn=Object.freeze({parseDirective:A}),_n=/[-.*+?^${}()|[\]\/\\]/g,yn=void 0,bn=void 0,wn=void 0,Cn=/[^|]\|[^|]/,$n=Object.freeze({compileRegex:T,parseText:N,tokensToExp:j}),kn=["{{","}}"],xn=["{{{","}}}"],An=Object.defineProperties({debug:!1,silent:!1,async:!0,warnExpressionErrors:!0,devtools:!1,_delimitersChanged:!0,_assetTypes:["component","directive","elementDirective","filter","transition","partial"],_propBindingModes:{ONE_WAY:0,TWO_WAY:1,ONE_TIME:2},_maxUpdateCount:100},{delimiters:{get:function(){return kn},set:function(t){kn=t,T()},configurable:!0,enumerable:!0},unsafeDelimiters:{get:function(){return xn},set:function(t){xn=t,T()},configurable:!0,enumerable:!0}}),On=void 0,Tn=Object.freeze({appendWithTransition:F,beforeWithTransition:D,removeWithTransition:P,applyTransition:R}),Nn=/^v-ref:/,jn=/^(div|p|span|img|a|b|i|br|ul|ol|li|h1|h2|h3|h4|h5|h6|code|pre|table|th|td|tr|form|label|input|select|option|nav|article|section|header|footer)$/i,En=/^(slot|partial|component)$/i,Sn=An.optionMergeStrategies=Object.create(null);Sn.data=function(t,e,i){return i?t||e?function(){var n="function"==typeof e?e.call(i):e,r="function"==typeof t?t.call(i):void 0;return n?ut(n,r):r}:void 0:e?"function"!=typeof e?t:t?function(){return ut(e.call(this),t.call(this))}:e:t},Sn.el=function(t,e,i){if(i||!e||"function"==typeof e){var n=e||t;return i&&"function"==typeof n?n.call(i):n}},Sn.init=Sn.created=Sn.ready=Sn.attached=Sn.detached=Sn.beforeCompile=Sn.compiled=Sn.beforeDestroy=Sn.destroyed=Sn.activate=function(t,e){return e?t?t.concat(e):Di(e)?e:[e]:t},An._assetTypes.forEach(function(t){Sn[t+"s"]=ft}),Sn.watch=Sn.events=function(t,e){if(!e)return t;if(!t)return e;var i={};v(i,t);for(var n in e){var r=i[n],s=e[n];r&&!Di(r)&&(r=[r]),i[n]=r?r.concat(s):[s]}return i},Sn.props=Sn.methods=Sn.computed=function(t,e){if(!e)return t;if(!t)return e;var i=Object.create(null);return v(i,t),v(i,e),i};var Fn=function(t,e){return void 0===e?t:e},Dn=0;_t.target=null,_t.prototype.addSub=function(t){this.subs.push(t)},_t.prototype.removeSub=function(t){this.subs.$remove(t)},_t.prototype.depend=function(){_t.target.addDep(this)},_t.prototype.notify=function(){for(var t=d(this.subs),e=0,i=t.length;i>e;e++)t[e].update()};var Pn=Array.prototype,Rn=Object.create(Pn);["push","pop","shift","unshift","splice","sort","reverse"].forEach(function(t){var e=Pn[t];_(Rn,t,function(){for(var i=arguments.length,n=new Array(i);i--;)n[i]=arguments[i];var r,s=e.apply(this,n),o=this.__ob__;switch(t){case"push":r=n;break;case"unshift":r=n;break;case"splice":r=n.slice(2)}return r&&o.observeArray(r),o.dep.notify(),s})}),_(Pn,"$set",function(t,e){return t>=this.length&&(this.length=Number(t)+1),this.splice(t,1,e)[0]}),_(Pn,"$remove",function(t){if(this.length){var e=b(this,t);return e>-1?this.splice(e,1):void 0}});var Ln=Object.getOwnPropertyNames(Rn),Hn=!0;bt.prototype.walk=function(t){for(var e=Object.keys(t),i=0,n=e.length;n>i;i++)this.convert(e[i],t[e[i]])},bt.prototype.observeArray=function(t){for(var e=0,i=t.length;i>e;e++)$t(t[e])},bt.prototype.convert=function(t,e){kt(this.value,t,e)},bt.prototype.addVm=function(t){(this.vms||(this.vms=[])).push(t)},bt.prototype.removeVm=function(t){this.vms.$remove(t)};var In=Object.freeze({defineReactive:kt,set:t,del:e,hasOwn:i,isLiteral:n,isReserved:r,_toString:s,toNumber:o,toBoolean:a,stripQuotes:h,camelize:l,hyphenate:u,classify:f,bind:p,toArray:d,extend:v,isObject:m,isPlainObject:g,def:_,debounce:y,indexOf:b,cancellable:w,looseEqual:C,isArray:Di,hasProto:Pi,inBrowser:Ri,devtools:Li,isIE:Ii,isIE9:Mi,isAndroid:Vi,isIos:Bi,iosVersionMatch:Wi,iosVersion:zi,hasMutationObserverBug:Ui,get transitionProp(){return Ji},get transitionEndEvent(){return qi},get animationProp(){return Qi},get animationEndEvent(){return Gi},nextTick:Yi,get _Set(){return Ki},query:L,inDoc:H,getAttr:I,getBindAttr:M,hasBindAttr:V,before:B,after:W,remove:z,prepend:U,replace:J,on:q,off:Q,setClass:Z,addClass:X,removeClass:Y,extractContent:K,trimNode:tt,isTemplate:it,createAnchor:nt,findRef:rt,mapNodeRange:st,removeNodeRange:ot,isFragment:at,getOuterHTML:ht,mergeOptions:mt,resolveAsset:gt,checkComponentAttr:lt,commonTagRE:jn,reservedTagRE:En,warn:On}),Mn=0,Vn=new $(1e3),Bn=0,Wn=1,zn=2,Un=3,Jn=0,qn=1,Qn=2,Gn=3,Zn=4,Xn=5,Yn=6,Kn=7,tr=8,er=[];er[Jn]={ws:[Jn],ident:[Gn,Bn],"[":[Zn],eof:[Kn]},er[qn]={ws:[qn],".":[Qn],"[":[Zn],eof:[Kn]},er[Qn]={ws:[Qn],ident:[Gn,Bn]},er[Gn]={ident:[Gn,Bn],0:[Gn,Bn],number:[Gn,Bn],ws:[qn,Wn],".":[Qn,Wn],"[":[Zn,Wn],eof:[Kn,Wn]},er[Zn]={"'":[Xn,Bn],'"':[Yn,Bn],"[":[Zn,zn],"]":[qn,Un],eof:tr,"else":[Zn,Bn]},er[Xn]={"'":[Zn,Bn],eof:tr,"else":[Xn,Bn]},er[Yn]={'"':[Zn,Bn],eof:tr,"else":[Yn,Bn]};var ir=Object.freeze({parsePath:Nt,getPath:jt,setPath:Et}),nr=new $(1e3),rr="Math,Date,this,true,false,null,undefined,Infinity,NaN,isNaN,isFinite,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,parseInt,parseFloat",sr=new RegExp("^("+rr.replace(/,/g,"\\b|")+"\\b)"),or="break,case,class,catch,const,continue,debugger,default,delete,do,else,export,extends,finally,for,function,if,import,in,instanceof,let,return,super,switch,throw,try,var,while,with,yield,enum,await,implements,package,protected,static,interface,private,public",ar=new RegExp("^("+or.replace(/,/g,"\\b|")+"\\b)"),hr=/\s/g,lr=/\n/g,cr=/[\{,]\s*[\w\$_]+\s*:|('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`)|new |typeof |void /g,ur=/"(\d+)"/g,fr=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*$/,pr=/[^\w$\.](?:[A-Za-z_$][\w$]*)/g,dr=/^(?:true|false|null|undefined|Infinity|NaN)$/,vr=[],mr=Object.freeze({parseExpression:It,isSimplePath:Mt}),gr=[],_r=[],yr={},br={},wr=!1,Cr=0;Ut.prototype.get=function(){this.beforeGet();var t,e=this.scope||this.vm;try{t=this.getter.call(e,e)}catch(i){}return this.deep&&Jt(t),this.preProcess&&(t=this.preProcess(t)),this.filters&&(t=e._applyFilters(t,null,this.filters,!1)),this.postProcess&&(t=this.postProcess(t)),this.afterGet(),t},Ut.prototype.set=function(t){var e=this.scope||this.vm;this.filters&&(t=e._applyFilters(t,this.value,this.filters,!0));try{this.setter.call(e,e,t)}catch(i){}var n=e.$forContext;if(n&&n.alias===this.expression){if(n.filters)return;n._withLock(function(){e.$key?n.rawValue[e.$key]=t:n.rawValue.$set(e.$index,t)})}},Ut.prototype.beforeGet=function(){_t.target=this},Ut.prototype.addDep=function(t){var e=t.id;this.newDepIds.has(e)||(this.newDepIds.add(e),this.newDeps.push(t),this.depIds.has(e)||t.addSub(this))},Ut.prototype.afterGet=function(){_t.target=null;for(var t=this.deps.length;t--;){var e=this.deps[t];this.newDepIds.has(e.id)||e.removeSub(this)}var i=this.depIds;this.depIds=this.newDepIds,this.newDepIds=i,this.newDepIds.clear(),i=this.deps,this.deps=this.newDeps,this.newDeps=i,this.newDeps.length=0},Ut.prototype.update=function(t){this.lazy?this.dirty=!0:this.sync||!An.async?this.run():(this.shallow=this.queued?t?this.shallow:!1:!!t,this.queued=!0,zt(this))},Ut.prototype.run=function(){if(this.active){var t=this.get();if(t!==this.value||(m(t)||this.deep)&&!this.shallow){var e=this.value;this.value=t;this.prevError;this.cb.call(this.vm,t,e)}this.queued=this.shallow=!1}},Ut.prototype.evaluate=function(){var t=_t.target;this.value=this.get(),this.dirty=!1,_t.target=t},Ut.prototype.depend=function(){for(var t=this.deps.length;t--;)this.deps[t].depend()},Ut.prototype.teardown=function(){if(this.active){this.vm._isBeingDestroyed||this.vm._vForRemoving||this.vm._watchers.$remove(this);for(var t=this.deps.length;t--;)this.deps[t].removeSub(this);this.active=!1,this.vm=this.cb=this.value=null}};var $r=new Ki,kr={bind:function(){this.attr=3===this.el.nodeType?"data":"textContent"},update:function(t){this.el[this.attr]=s(t)}},xr=new $(1e3),Ar=new $(1e3),Or={efault:[0,"",""],legend:[1,"<fieldset>","</fieldset>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"]};Or.td=Or.th=[3,"<table><tbody><tr>","</tr></tbody></table>"],Or.option=Or.optgroup=[1,'<select multiple="multiple">',"</select>"],Or.thead=Or.tbody=Or.colgroup=Or.caption=Or.tfoot=[1,"<table>","</table>"],Or.g=Or.defs=Or.symbol=Or.use=Or.image=Or.text=Or.circle=Or.ellipse=Or.line=Or.path=Or.polygon=Or.polyline=Or.rect=[1,'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events"version="1.1">',"</svg>"];var Tr=/<([\w:-]+)/,Nr=/&#?\w+?;/,jr=/<!--/,Er=function(){if(Ri){var t=document.createElement("div");return t.innerHTML="<template>1</template>",!t.cloneNode(!0).firstChild.innerHTML}return!1}(),Sr=function(){if(Ri){var t=document.createElement("textarea");return t.placeholder="t","t"===t.cloneNode(!0).value}return!1}(),Fr=Object.freeze({cloneNode:Zt,parseTemplate:Xt}),Dr={bind:function(){8===this.el.nodeType&&(this.nodes=[],this.anchor=nt("v-html"),J(this.el,this.anchor))},update:function(t){t=s(t),this.nodes?this.swap(t):this.el.innerHTML=t},swap:function(t){for(var e=this.nodes.length;e--;)z(this.nodes[e]);var i=Xt(t,!0,!0);this.nodes=d(i.childNodes),B(i,this.anchor)}};Yt.prototype.callHook=function(t){var e,i;for(e=0,i=this.childFrags.length;i>e;e++)this.childFrags[e].callHook(t);for(e=0,i=this.children.length;i>e;e++)t(this.children[e])},Yt.prototype.beforeRemove=function(){var t,e;for(t=0,e=this.childFrags.length;e>t;t++)this.childFrags[t].beforeRemove(!1);for(t=0,e=this.children.length;e>t;t++)this.children[t].$destroy(!1,!0);var i=this.unlink.dirs;for(t=0,e=i.length;e>t;t++)i[t]._watcher&&i[t]._watcher.teardown()},Yt.prototype.destroy=function(){this.parentFrag&&this.parentFrag.childFrags.$remove(this),this.node.__v_frag=null,this.unlink()};var Pr=new $(5e3);se.prototype.create=function(t,e,i){var n=Zt(this.template);return new Yt(this.linker,this.vm,n,t,e,i)};var Rr=700,Lr=800,Hr=850,Ir=1100,Mr=1500,Vr=1500,Br=1750,Wr=2100,zr=2200,Ur=2300,Jr=0,qr={priority:zr,terminal:!0,params:["track-by","stagger","enter-stagger","leave-stagger"],bind:function(){var t=this.expression.match(/(.*) (?:in|of) (.*)/);if(t){var e=t[1].match(/\((.*),(.*)\)/);e?(this.iterator=e[1].trim(),this.alias=e[2].trim()):this.alias=t[1].trim(),this.expression=t[2]}if(this.alias){this.id="__v-for__"+ ++Jr;var i=this.el.tagName;this.isOption=("OPTION"===i||"OPTGROUP"===i)&&"SELECT"===this.el.parentNode.tagName,this.start=nt("v-for-start"),this.end=nt("v-for-end"),J(this.el,this.end),B(this.start,this.end),this.cache=Object.create(null),this.factory=new se(this.vm,this.el)}},update:function(t){this.diff(t),this.updateRef(),this.updateModel()},diff:function(t){var e,n,r,s,o,a,h=t[0],l=this.fromObject=m(h)&&i(h,"$key")&&i(h,"$value"),c=this.params.trackBy,u=this.frags,f=this.frags=new Array(t.length),p=this.alias,d=this.iterator,v=this.start,g=this.end,_=H(v),y=!u;for(e=0,n=t.length;n>e;e++)h=t[e],s=l?h.$key:null,o=l?h.$value:h,a=!m(o),r=!y&&this.getCachedFrag(o,e,s),r?(r.reused=!0,r.scope.$index=e,s&&(r.scope.$key=s),d&&(r.scope[d]=null!==s?s:e),(c||l||a)&&yt(function(){r.scope[p]=o})):(r=this.create(o,p,e,s),r.fresh=!y),f[e]=r,y&&r.before(g);if(!y){var b=0,w=u.length-f.length;for(this.vm._vForRemoving=!0,e=0,n=u.length;n>e;e++)r=u[e],r.reused||(this.deleteCachedFrag(r),this.remove(r,b++,w,_));this.vm._vForRemoving=!1,b&&(this.vm._watchers=this.vm._watchers.filter(function(t){return t.active}));var C,$,k,x=0;for(e=0,n=f.length;n>e;e++)r=f[e],C=f[e-1],$=C?C.staggerCb?C.staggerAnchor:C.end||C.node:v,r.reused&&!r.staggerCb?(k=oe(r,v,this.id),k===C||k&&oe(k,v,this.id)===C||this.move(r,$)):this.insert(r,x++,$,_),r.reused=r.fresh=!1}},create:function(t,e,i,n){var r=this._host,s=this._scope||this.vm,o=Object.create(s);o.$refs=Object.create(s.$refs),o.$els=Object.create(s.$els),o.$parent=s,o.$forContext=this,yt(function(){kt(o,e,t)}),kt(o,"$index",i),n?kt(o,"$key",n):o.$key&&_(o,"$key",null),this.iterator&&kt(o,this.iterator,null!==n?n:i);var a=this.factory.create(r,o,this._frag);return a.forId=this.id,this.cacheFrag(t,a,i,n),a},updateRef:function(){var t=this.descriptor.ref;if(t){var e,i=(this._scope||this.vm).$refs;this.fromObject?(e={},this.frags.forEach(function(t){e[t.scope.$key]=ae(t)})):e=this.frags.map(ae),i[t]=e}},updateModel:function(){if(this.isOption){var t=this.start.parentNode,e=t&&t.__v_model;e&&e.forceUpdate()}},insert:function(t,e,i,n){t.staggerCb&&(t.staggerCb.cancel(),t.staggerCb=null);var r=this.getStagger(t,e,null,"enter");if(n&&r){var s=t.staggerAnchor;s||(s=t.staggerAnchor=nt("stagger-anchor"),s.__v_frag=t),W(s,i);var o=t.staggerCb=w(function(){t.staggerCb=null,t.before(s),z(s)});setTimeout(o,r)}else{var a=i.nextSibling;a||(W(this.end,i),a=this.end),t.before(a)}},remove:function(t,e,i,n){if(t.staggerCb)return t.staggerCb.cancel(),void(t.staggerCb=null);var r=this.getStagger(t,e,i,"leave");if(n&&r){var s=t.staggerCb=w(function(){t.staggerCb=null,t.remove()});setTimeout(s,r)}else t.remove()},move:function(t,e){e.nextSibling||this.end.parentNode.appendChild(this.end),t.before(e.nextSibling,!1)},cacheFrag:function(t,e,n,r){var s,o=this.params.trackBy,a=this.cache,h=!m(t);r||o||h?(s=le(n,r,t,o),a[s]||(a[s]=e)):(s=this.id,i(t,s)?null===t[s]&&(t[s]=e):Object.isExtensible(t)&&_(t,s,e)),e.raw=t},getCachedFrag:function(t,e,i){var n,r=this.params.trackBy,s=!m(t);if(i||r||s){var o=le(e,i,t,r);n=this.cache[o]}else n=t[this.id];return n&&(n.reused||n.fresh),n},deleteCachedFrag:function(t){var e=t.raw,n=this.params.trackBy,r=t.scope,s=r.$index,o=i(r,"$key")&&r.$key,a=!m(e);if(n||o||a){var h=le(s,o,e,n);this.cache[h]=null}else e[this.id]=null,t.raw=null},getStagger:function(t,e,i,n){n+="Stagger";var r=t.node.__v_trans,s=r&&r.hooks,o=s&&(s[n]||s.stagger);return o?o.call(t,e,i):e*parseInt(this.params[n]||this.params.stagger,10)},_preProcess:function(t){return this.rawValue=t,t},_postProcess:function(t){if(Di(t))return t;if(g(t)){for(var e,i=Object.keys(t),n=i.length,r=new Array(n);n--;)e=i[n],r[n]={$key:e,$value:t[e]};return r}return"number"!=typeof t||isNaN(t)||(t=he(t)),t||[]},unbind:function(){if(this.descriptor.ref&&((this._scope||this.vm).$refs[this.descriptor.ref]=null),this.frags)for(var t,e=this.frags.length;e--;)t=this.frags[e],this.deleteCachedFrag(t),t.destroy()}},Qr={priority:Wr,terminal:!0,bind:function(){var t=this.el;if(t.__vue__)this.invalid=!0;else{var e=t.nextElementSibling;e&&null!==I(e,"v-else")&&(z(e),this.elseEl=e),this.anchor=nt("v-if"),J(t,this.anchor)}},update:function(t){this.invalid||(t?this.frag||this.insert():this.remove())},insert:function(){this.elseFrag&&(this.elseFrag.remove(),this.elseFrag=null),this.factory||(this.factory=new se(this.vm,this.el)),this.frag=this.factory.create(this._host,this._scope,this._frag),this.frag.before(this.anchor)},remove:function(){this.frag&&(this.frag.remove(),this.frag=null),this.elseEl&&!this.elseFrag&&(this.elseFactory||(this.elseFactory=new se(this.elseEl._context||this.vm,this.elseEl)),this.elseFrag=this.elseFactory.create(this._host,this._scope,this._frag),this.elseFrag.before(this.anchor))},unbind:function(){this.frag&&this.frag.destroy(),this.elseFrag&&this.elseFrag.destroy()}},Gr={bind:function(){var t=this.el.nextElementSibling;t&&null!==I(t,"v-else")&&(this.elseEl=t)},update:function(t){this.apply(this.el,t),this.elseEl&&this.apply(this.elseEl,!t)},apply:function(t,e){function i(){t.style.display=e?"":"none"}H(t)?R(t,e?1:-1,i,this.vm):i()}},Zr={bind:function(){var t=this,e=this.el,i="range"===e.type,n=this.params.lazy,r=this.params.number,s=this.params.debounce,a=!1;if(Vi||i||(this.on("compositionstart",function(){a=!0}),this.on("compositionend",function(){a=!1,n||t.listener()})),this.focused=!1,i||n||(this.on("focus",function(){t.focused=!0}),this.on("blur",function(){t.focused=!1,t._frag&&!t._frag.inserted||t.rawListener()})),this.listener=this.rawListener=function(){if(!a&&t._bound){var n=r||i?o(e.value):e.value;t.set(n),Yi(function(){t._bound&&!t.focused&&t.update(t._watcher.value)})}},s&&(this.listener=y(this.listener,s)),this.hasjQuery="function"==typeof jQuery,this.hasjQuery){var h=jQuery.fn.on?"on":"bind";jQuery(e)[h]("change",this.rawListener),n||jQuery(e)[h]("input",this.listener)}else this.on("change",this.rawListener),n||this.on("input",this.listener);!n&&Mi&&(this.on("cut",function(){Yi(t.listener)}),this.on("keyup",function(e){46!==e.keyCode&&8!==e.keyCode||t.listener()})),(e.hasAttribute("value")||"TEXTAREA"===e.tagName&&e.value.trim())&&(this.afterBind=this.listener)},update:function(t){t=s(t),t!==this.el.value&&(this.el.value=t)},unbind:function(){var t=this.el;if(this.hasjQuery){var e=jQuery.fn.off?"off":"unbind";jQuery(t)[e]("change",this.listener),jQuery(t)[e]("input",this.listener)}}},Xr={bind:function(){var t=this,e=this.el;this.getValue=function(){if(e.hasOwnProperty("_value"))return e._value;var i=e.value;return t.params.number&&(i=o(i)),i},this.listener=function(){t.set(t.getValue())},this.on("change",this.listener),e.hasAttribute("checked")&&(this.afterBind=this.listener)},update:function(t){this.el.checked=C(t,this.getValue())}},Yr={bind:function(){var t=this,e=this,i=this.el;this.forceUpdate=function(){e._watcher&&e.update(e._watcher.get())};var n=this.multiple=i.hasAttribute("multiple");this.listener=function(){var t=ce(i,n);t=e.params.number?Di(t)?t.map(o):o(t):t,e.set(t)},this.on("change",this.listener);var r=ce(i,n,!0);(n&&r.length||!n&&null!==r)&&(this.afterBind=this.listener),this.vm.$on("hook:attached",function(){Yi(t.forceUpdate)}),H(i)||Yi(this.forceUpdate)},update:function(t){var e=this.el;e.selectedIndex=-1;for(var i,n,r=this.multiple&&Di(t),s=e.options,o=s.length;o--;)i=s[o],n=i.hasOwnProperty("_value")?i._value:i.value,i.selected=r?ue(t,n)>-1:C(t,n)},unbind:function(){this.vm.$off("hook:attached",this.forceUpdate)}},Kr={bind:function(){function t(){var t=i.checked;return t&&i.hasOwnProperty("_trueValue")?i._trueValue:!t&&i.hasOwnProperty("_falseValue")?i._falseValue:t}var e=this,i=this.el;this.getValue=function(){return i.hasOwnProperty("_value")?i._value:e.params.number?o(i.value):i.value},this.listener=function(){var n=e._watcher.value;if(Di(n)){var r=e.getValue();i.checked?b(n,r)<0&&n.push(r):n.$remove(r)}else e.set(t())},this.on("change",this.listener),i.hasAttribute("checked")&&(this.afterBind=this.listener)},update:function(t){var e=this.el;Di(t)?e.checked=b(t,this.getValue())>-1:e.hasOwnProperty("_trueValue")?e.checked=C(t,e._trueValue):e.checked=!!t}},ts={text:Zr,radio:Xr,select:Yr,checkbox:Kr},es={priority:Lr,twoWay:!0,handlers:ts,params:["lazy","number","debounce"],bind:function(){this.checkFilters(),this.hasRead&&!this.hasWrite;var t,e=this.el,i=e.tagName;if("INPUT"===i)t=ts[e.type]||ts.text;else if("SELECT"===i)t=ts.select;else{if("TEXTAREA"!==i)return;t=ts.text}e.__v_model=this,t.bind.call(this),this.update=t.update,this._unbind=t.unbind},checkFilters:function(){var t=this.filters;if(t)for(var e=t.length;e--;){var i=gt(this.vm.$options,"filters",t[e].name);("function"==typeof i||i.read)&&(this.hasRead=!0),i.write&&(this.hasWrite=!0)}},unbind:function(){this.el.__v_model=null,this._unbind&&this._unbind()}},is={esc:27,tab:9,enter:13,space:32,"delete":[8,46],up:38,left:37,right:39,down:40},ns={priority:Rr,acceptStatement:!0,keyCodes:is,bind:function(){if("IFRAME"===this.el.tagName&&"load"!==this.arg){var t=this;this.iframeBind=function(){q(t.el.contentWindow,t.arg,t.handler,t.modifiers.capture)},this.on("load",this.iframeBind)}},update:function(t){if(this.descriptor.raw||(t=function(){}),"function"==typeof t){this.modifiers.stop&&(t=pe(t)),this.modifiers.prevent&&(t=de(t)),this.modifiers.self&&(t=ve(t));var e=Object.keys(this.modifiers).filter(function(t){return"stop"!==t&&"prevent"!==t&&"self"!==t&&"capture"!==t});e.length&&(t=fe(t,e)),this.reset(),this.handler=t,this.iframeBind?this.iframeBind():q(this.el,this.arg,this.handler,this.modifiers.capture)}},reset:function(){var t=this.iframeBind?this.el.contentWindow:this.el;this.handler&&Q(t,this.arg,this.handler)},unbind:function(){this.reset()}},rs=["-webkit-","-moz-","-ms-"],ss=["Webkit","Moz","ms"],os=/!important;?$/,as=Object.create(null),hs=null,ls={deep:!0,update:function(t){"string"==typeof t?this.el.style.cssText=t:Di(t)?this.handleObject(t.reduce(v,{})):this.handleObject(t||{})},handleObject:function(t){var e,i,n=this.cache||(this.cache={});for(e in n)e in t||(this.handleSingle(e,null),delete n[e]);for(e in t)i=t[e],i!==n[e]&&(n[e]=i,this.handleSingle(e,i))},handleSingle:function(t,e){if(t=me(t))if(null!=e&&(e+=""),e){var i=os.test(e)?"important":"";i?(e=e.replace(os,"").trim(),this.el.style.setProperty(t.kebab,e,i)):this.el.style[t.camel]=e}else this.el.style[t.camel]=""}},cs="http://www.w3.org/1999/xlink",us=/^xlink:/,fs=/^v-|^:|^@|^(?:is|transition|transition-mode|debounce|track-by|stagger|enter-stagger|leave-stagger)$/,ps=/^(?:value|checked|selected|muted)$/,ds=/^(?:draggable|contenteditable|spellcheck)$/,vs={value:"_value","true-value":"_trueValue","false-value":"_falseValue"},ms={priority:Hr,bind:function(){var t=this.arg,e=this.el.tagName;t||(this.deep=!0);var i=this.descriptor,n=i.interp;n&&(i.hasOneTime&&(this.expression=j(n,this._scope||this.vm)),(fs.test(t)||"name"===t&&("PARTIAL"===e||"SLOT"===e))&&(this.el.removeAttribute(t),this.invalid=!0))},update:function(t){
-if(!this.invalid){var e=this.arg;this.arg?this.handleSingle(e,t):this.handleObject(t||{})}},handleObject:ls.handleObject,handleSingle:function(t,e){var i=this.el,n=this.descriptor.interp;if(this.modifiers.camel&&(t=l(t)),!n&&ps.test(t)&&t in i){var r="value"===t&&null==e?"":e;i[t]!==r&&(i[t]=r)}var s=vs[t];if(!n&&s){i[s]=e;var o=i.__v_model;o&&o.listener()}return"value"===t&&"TEXTAREA"===i.tagName?void i.removeAttribute(t):void(ds.test(t)?i.setAttribute(t,e?"true":"false"):null!=e&&e!==!1?"class"===t?(i.__v_trans&&(e+=" "+i.__v_trans.id+"-transition"),Z(i,e)):us.test(t)?i.setAttributeNS(cs,t,e===!0?"":e):i.setAttribute(t,e===!0?"":e):i.removeAttribute(t))}},gs={priority:Mr,bind:function(){if(this.arg){var t=this.id=l(this.arg),e=(this._scope||this.vm).$els;i(e,t)?e[t]=this.el:kt(e,t,this.el)}},unbind:function(){var t=(this._scope||this.vm).$els;t[this.id]===this.el&&(t[this.id]=null)}},_s={bind:function(){}},ys={bind:function(){var t=this.el;this.vm.$once("pre-hook:compiled",function(){t.removeAttribute("v-cloak")})}},bs={text:kr,html:Dr,"for":qr,"if":Qr,show:Gr,model:es,on:ns,bind:ms,el:gs,ref:_s,cloak:ys},ws={deep:!0,update:function(t){t?"string"==typeof t?this.setClass(t.trim().split(/\s+/)):this.setClass(_e(t)):this.cleanup()},setClass:function(t){this.cleanup(t);for(var e=0,i=t.length;i>e;e++){var n=t[e];n&&ye(this.el,n,X)}this.prevKeys=t},cleanup:function(t){var e=this.prevKeys;if(e)for(var i=e.length;i--;){var n=e[i];(!t||t.indexOf(n)<0)&&ye(this.el,n,Y)}}},Cs={priority:Vr,params:["keep-alive","transition-mode","inline-template"],bind:function(){this.el.__vue__||(this.keepAlive=this.params.keepAlive,this.keepAlive&&(this.cache={}),this.params.inlineTemplate&&(this.inlineTemplate=K(this.el,!0)),this.pendingComponentCb=this.Component=null,this.pendingRemovals=0,this.pendingRemovalCb=null,this.anchor=nt("v-component"),J(this.el,this.anchor),this.el.removeAttribute("is"),this.el.removeAttribute(":is"),this.descriptor.ref&&this.el.removeAttribute("v-ref:"+u(this.descriptor.ref)),this.literal&&this.setComponent(this.expression))},update:function(t){this.literal||this.setComponent(t)},setComponent:function(t,e){if(this.invalidatePending(),t){var i=this;this.resolveComponent(t,function(){i.mountComponent(e)})}else this.unbuild(!0),this.remove(this.childVM,e),this.childVM=null},resolveComponent:function(t,e){var i=this;this.pendingComponentCb=w(function(n){i.ComponentName=n.options.name||("string"==typeof t?t:null),i.Component=n,e()}),this.vm._resolveComponent(t,this.pendingComponentCb)},mountComponent:function(t){this.unbuild(!0);var e=this,i=this.Component.options.activate,n=this.getCached(),r=this.build();i&&!n?(this.waitingFor=r,be(i,r,function(){e.waitingFor===r&&(e.waitingFor=null,e.transition(r,t))})):(n&&r._updateRef(),this.transition(r,t))},invalidatePending:function(){this.pendingComponentCb&&(this.pendingComponentCb.cancel(),this.pendingComponentCb=null)},build:function(t){var e=this.getCached();if(e)return e;if(this.Component){var i={name:this.ComponentName,el:Zt(this.el),template:this.inlineTemplate,parent:this._host||this.vm,_linkerCachable:!this.inlineTemplate,_ref:this.descriptor.ref,_asComponent:!0,_isRouterView:this._isRouterView,_context:this.vm,_scope:this._scope,_frag:this._frag};t&&v(i,t);var n=new this.Component(i);return this.keepAlive&&(this.cache[this.Component.cid]=n),n}},getCached:function(){return this.keepAlive&&this.cache[this.Component.cid]},unbuild:function(t){this.waitingFor&&(this.keepAlive||this.waitingFor.$destroy(),this.waitingFor=null);var e=this.childVM;return!e||this.keepAlive?void(e&&(e._inactive=!0,e._updateRef(!0))):void e.$destroy(!1,t)},remove:function(t,e){var i=this.keepAlive;if(t){this.pendingRemovals++,this.pendingRemovalCb=e;var n=this;t.$remove(function(){n.pendingRemovals--,i||t._cleanup(),!n.pendingRemovals&&n.pendingRemovalCb&&(n.pendingRemovalCb(),n.pendingRemovalCb=null)})}else e&&e()},transition:function(t,e){var i=this,n=this.childVM;switch(n&&(n._inactive=!0),t._inactive=!1,this.childVM=t,i.params.transitionMode){case"in-out":t.$before(i.anchor,function(){i.remove(n,e)});break;case"out-in":i.remove(n,function(){t.$before(i.anchor,e)});break;default:i.remove(n),t.$before(i.anchor,e)}},unbind:function(){if(this.invalidatePending(),this.unbuild(),this.cache){for(var t in this.cache)this.cache[t].$destroy();this.cache=null}}},$s=An._propBindingModes,ks={},xs=/^[$_a-zA-Z]+[\w$]*$/,As=An._propBindingModes,Os={bind:function(){var t=this.vm,e=t._context,i=this.descriptor.prop,n=i.path,r=i.parentPath,s=i.mode===As.TWO_WAY,o=this.parentWatcher=new Ut(e,r,function(e){xe(t,i,e)},{twoWay:s,filters:i.filters,scope:this._scope});if(ke(t,i,o.value),s){var a=this;t.$once("pre-hook:created",function(){a.childWatcher=new Ut(t,n,function(t){o.set(t)},{sync:!0})})}},unbind:function(){this.parentWatcher.teardown(),this.childWatcher&&this.childWatcher.teardown()}},Ts=[],Ns=!1,js="transition",Es="animation",Ss=Ji+"Duration",Fs=Qi+"Duration",Ds=Ri&&window.requestAnimationFrame,Ps=Ds?function(t){Ds(function(){Ds(t)})}:function(t){setTimeout(t,50)},Rs=Se.prototype;Rs.enter=function(t,e){this.cancelPending(),this.callHook("beforeEnter"),this.cb=e,X(this.el,this.enterClass),t(),this.entered=!1,this.callHookWithCb("enter"),this.entered||(this.cancel=this.hooks&&this.hooks.enterCancelled,je(this.enterNextTick))},Rs.enterNextTick=function(){var t=this;this.justEntered=!0,Ps(function(){t.justEntered=!1});var e=this.enterDone,i=this.getCssTransitionType(this.enterClass);this.pendingJsCb?i===js&&Y(this.el,this.enterClass):i===js?(Y(this.el,this.enterClass),this.setupCssCb(qi,e)):i===Es?this.setupCssCb(Gi,e):e()},Rs.enterDone=function(){this.entered=!0,this.cancel=this.pendingJsCb=null,Y(this.el,this.enterClass),this.callHook("afterEnter"),this.cb&&this.cb()},Rs.leave=function(t,e){this.cancelPending(),this.callHook("beforeLeave"),this.op=t,this.cb=e,X(this.el,this.leaveClass),this.left=!1,this.callHookWithCb("leave"),this.left||(this.cancel=this.hooks&&this.hooks.leaveCancelled,this.op&&!this.pendingJsCb&&(this.justEntered?this.leaveDone():je(this.leaveNextTick)))},Rs.leaveNextTick=function(){var t=this.getCssTransitionType(this.leaveClass);if(t){var e=t===js?qi:Gi;this.setupCssCb(e,this.leaveDone)}else this.leaveDone()},Rs.leaveDone=function(){this.left=!0,this.cancel=this.pendingJsCb=null,this.op(),Y(this.el,this.leaveClass),this.callHook("afterLeave"),this.cb&&this.cb(),this.op=null},Rs.cancelPending=function(){this.op=this.cb=null;var t=!1;this.pendingCssCb&&(t=!0,Q(this.el,this.pendingCssEvent,this.pendingCssCb),this.pendingCssEvent=this.pendingCssCb=null),this.pendingJsCb&&(t=!0,this.pendingJsCb.cancel(),this.pendingJsCb=null),t&&(Y(this.el,this.enterClass),Y(this.el,this.leaveClass)),this.cancel&&(this.cancel.call(this.vm,this.el),this.cancel=null)},Rs.callHook=function(t){this.hooks&&this.hooks[t]&&this.hooks[t].call(this.vm,this.el)},Rs.callHookWithCb=function(t){var e=this.hooks&&this.hooks[t];e&&(e.length>1&&(this.pendingJsCb=w(this[t+"Done"])),e.call(this.vm,this.el,this.pendingJsCb))},Rs.getCssTransitionType=function(t){if(!(!qi||document.hidden||this.hooks&&this.hooks.css===!1||Fe(this.el))){var e=this.type||this.typeCache[t];if(e)return e;var i=this.el.style,n=window.getComputedStyle(this.el),r=i[Ss]||n[Ss];if(r&&"0s"!==r)e=js;else{var s=i[Fs]||n[Fs];s&&"0s"!==s&&(e=Es)}return e&&(this.typeCache[t]=e),e}},Rs.setupCssCb=function(t,e){this.pendingCssEvent=t;var i=this,n=this.el,r=this.pendingCssCb=function(s){s.target===n&&(Q(n,t,r),i.pendingCssEvent=i.pendingCssCb=null,!i.pendingJsCb&&e&&e())};q(n,t,r)};var Ls={priority:Ir,update:function(t,e){var i=this.el,n=gt(this.vm.$options,"transitions",t);t=t||"v",e=e||"v",i.__v_trans=new Se(i,t,n,this.vm),Y(i,e+"-transition"),X(i,t+"-transition")}},Hs={style:ls,"class":ws,component:Cs,prop:Os,transition:Ls},Is=/^v-bind:|^:/,Ms=/^v-on:|^@/,Vs=/^v-([^:]+)(?:$|:(.*)$)/,Bs=/\.[^\.]+/g,Ws=/^(v-bind:|:)?transition$/,zs=1e3,Us=2e3;Ye.terminal=!0;var Js=/[^\w\-:\.]/,qs=Object.freeze({compile:De,compileAndLinkProps:Ie,compileRoot:Me,transclude:si,resolveSlots:li}),Qs=/^v-on:|^@/;di.prototype._bind=function(){var t=this.name,e=this.descriptor;if(("cloak"!==t||this.vm._isCompiled)&&this.el&&this.el.removeAttribute){var i=e.attr||"v-"+t;this.el.removeAttribute(i)}var n=e.def;if("function"==typeof n?this.update=n:v(this,n),this._setupParams(),this.bind&&this.bind(),this._bound=!0,this.literal)this.update&&this.update(e.raw);else if((this.expression||this.modifiers)&&(this.update||this.twoWay)&&!this._checkStatement()){var r=this;this.update?this._update=function(t,e){r._locked||r.update(t,e)}:this._update=pi;var s=this._preProcess?p(this._preProcess,this):null,o=this._postProcess?p(this._postProcess,this):null,a=this._watcher=new Ut(this.vm,this.expression,this._update,{filters:this.filters,twoWay:this.twoWay,deep:this.deep,preProcess:s,postProcess:o,scope:this._scope});this.afterBind?this.afterBind():this.update&&this.update(a.value)}},di.prototype._setupParams=function(){if(this.params){var t=this.params;this.params=Object.create(null);for(var e,i,n,r=t.length;r--;)e=u(t[r]),n=l(e),i=M(this.el,e),null!=i?this._setupParamWatcher(n,i):(i=I(this.el,e),null!=i&&(this.params[n]=""===i?!0:i))}},di.prototype._setupParamWatcher=function(t,e){var i=this,n=!1,r=(this._scope||this.vm).$watch(e,function(e,r){if(i.params[t]=e,n){var s=i.paramWatchers&&i.paramWatchers[t];s&&s.call(i,e,r)}else n=!0},{immediate:!0,user:!1});(this._paramUnwatchFns||(this._paramUnwatchFns=[])).push(r)},di.prototype._checkStatement=function(){var t=this.expression;if(t&&this.acceptStatement&&!Mt(t)){var e=It(t).get,i=this._scope||this.vm,n=function(t){i.$event=t,e.call(i,i),i.$event=null};return this.filters&&(n=i._applyFilters(n,null,this.filters)),this.update(n),!0}},di.prototype.set=function(t){this.twoWay&&this._withLock(function(){this._watcher.set(t)})},di.prototype._withLock=function(t){var e=this;e._locked=!0,t.call(e),Yi(function(){e._locked=!1})},di.prototype.on=function(t,e,i){q(this.el,t,e,i),(this._listeners||(this._listeners=[])).push([t,e])},di.prototype._teardown=function(){if(this._bound){this._bound=!1,this.unbind&&this.unbind(),this._watcher&&this._watcher.teardown();var t,e=this._listeners;if(e)for(t=e.length;t--;)Q(this.el,e[t][0],e[t][1]);var i=this._paramUnwatchFns;if(i)for(t=i.length;t--;)i[t]();this.vm=this.el=this._watcher=this._listeners=null}};var Gs=/[^|]\|[^|]/;xt(wi),ui(wi),fi(wi),vi(wi),mi(wi),gi(wi),_i(wi),yi(wi),bi(wi);var Zs={priority:Ur,params:["name"],bind:function(){var t=this.params.name||"default",e=this.vm._slotContents&&this.vm._slotContents[t];e&&e.hasChildNodes()?this.compile(e.cloneNode(!0),this.vm._context,this.vm):this.fallback()},compile:function(t,e,i){if(t&&e){if(this.el.hasChildNodes()&&1===t.childNodes.length&&1===t.childNodes[0].nodeType&&t.childNodes[0].hasAttribute("v-if")){var n=document.createElement("template");n.setAttribute("v-else",""),n.innerHTML=this.el.innerHTML,n._context=this.vm,t.appendChild(n)}var r=i?i._scope:this._scope;this.unlink=e.$compile(t,i,r,this._frag)}t?J(this.el,t):z(this.el)},fallback:function(){this.compile(K(this.el,!0),this.vm)},unbind:function(){this.unlink&&this.unlink()}},Xs={priority:Br,params:["name"],paramWatchers:{name:function(t){Qr.remove.call(this),t&&this.insert(t)}},bind:function(){this.anchor=nt("v-partial"),J(this.el,this.anchor),this.insert(this.params.name)},insert:function(t){var e=gt(this.vm.$options,"partials",t,!0);e&&(this.factory=new se(this.vm,e),Qr.insert.call(this))},unbind:function(){this.frag&&this.frag.destroy()}},Ys={slot:Zs,partial:Xs},Ks=qr._postProcess,to=/(\d{3})(?=\d)/g,eo={orderBy:ki,filterBy:$i,limitBy:Ci,json:{read:function(t,e){return"string"==typeof t?t:JSON.stringify(t,null,arguments.length>1?e:2)},write:function(t){try{return JSON.parse(t)}catch(e){return t}}},capitalize:function(t){return t||0===t?(t=t.toString(),t.charAt(0).toUpperCase()+t.slice(1)):""},uppercase:function(t){return t||0===t?t.toString().toUpperCase():""},lowercase:function(t){return t||0===t?t.toString().toLowerCase():""},currency:function(t,e,i){if(t=parseFloat(t),!isFinite(t)||!t&&0!==t)return"";e=null!=e?e:"$",i=null!=i?i:2;var n=Math.abs(t).toFixed(i),r=i?n.slice(0,-1-i):n,s=r.length%3,o=s>0?r.slice(0,s)+(r.length>3?",":""):"",a=i?n.slice(-1-i):"",h=0>t?"-":"";return h+e+o+r.slice(s).replace(to,"$1,")+a},pluralize:function(t){var e=d(arguments,1),i=e.length;if(i>1){var n=t%10-1;return n in e?e[n]:e[i-1]}return e[0]+(1===t?"":"s")},debounce:function(t,e){return t?(e||(e=300),y(t,e)):void 0}};return Ai(wi),wi.version="1.0.26",setTimeout(function(){An.devtools&&Li&&Li.emit("init",wi)},0),wi});
-//# sourceMappingURL=vue.min.js.map \ No newline at end of file
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Vue=t()}(this,function(){"use strict";function e(e){return null==e?"":"object"==typeof e?JSON.stringify(e,null,2):String(e)}function t(e){var t=parseFloat(e,10);return t||0===t?t:e}function n(e,t){for(var n=Object.create(null),r=e.split(","),i=0;i<r.length;i++)n[r[i]]=!0;return t?function(e){return n[e.toLowerCase()]}:function(e){return n[e]}}function r(e,t){if(e.length){var n=e.indexOf(t);if(n>-1)return e.splice(n,1)}}function i(e,t){return _r.call(e,t)}function a(e){return"string"==typeof e||"number"==typeof e}function o(e){var t=Object.create(null);return function(n){var r=t[n];return r||(t[n]=e(n))}}function s(e,t){function n(n){var r=arguments.length;return r?r>1?e.apply(t,arguments):e.call(t,n):e.call(t)}return n._length=e.length,n}function c(e,t){t=t||0;for(var n=e.length-t,r=new Array(n);n--;)r[n]=e[n+t];return r}function u(e,t){for(var n in t)e[n]=t[n];return e}function l(e){return null!==e&&"object"==typeof e}function f(e){return kr.call(e)===Ar}function d(e){for(var t={},n=0;n<e.length;n++)e[n]&&u(t,e[n]);return t}function p(){}function v(e){return e.reduce(function(e,t){return e.concat(t.staticKeys||[])},[]).join(",")}function h(e,t){return e==t||!(!l(e)||!l(t))&&JSON.stringify(e)===JSON.stringify(t)}function m(e,t){for(var n=0;n<e.length;n++)if(h(e[n],t))return n;return-1}function g(e){var t=(e+"").charCodeAt(0);return 36===t||95===t}function y(e,t,n,r){Object.defineProperty(e,t,{value:n,enumerable:!!r,writable:!0,configurable:!0})}function _(e){if(!Sr.test(e)){var t=e.split(".");return function(e){for(var n=0;n<t.length;n++){if(!e)return;e=e[t[n]]}return e}}}function b(e){return/native code/.test(e.toString())}function $(e){Hr.target&&Ur.push(Hr.target),Hr.target=e}function w(){Hr.target=Ur.pop()}function C(){zr.length=0,Vr={},Jr=qr=!1}function x(){for(qr=!0,zr.sort(function(e,t){return e.id-t.id}),Kr=0;Kr<zr.length;Kr++){var e=zr[Kr],t=e.id;Vr[t]=null,e.run()}Ir&&Tr.devtools&&Ir.emit("flush"),C()}function k(e){var t=e.id;if(null==Vr[t]){if(Vr[t]=!0,qr){for(var n=zr.length-1;n>=0&&zr[n].id>e.id;)n--;zr.splice(Math.max(n,Kr)+1,0,e)}else zr.push(e);Jr||(Jr=!0,Br(x))}}function A(e,t){var n,r;t||(t=Gr,t.clear());var i=Array.isArray(e),a=l(e);if((i||a)&&Object.isExtensible(e)){if(e.__ob__){var o=e.__ob__.dep.id;if(t.has(o))return;t.add(o)}if(i)for(n=e.length;n--;)A(e[n],t);else if(a)for(r=Object.keys(e),n=r.length;n--;)A(e[r[n]],t)}}function O(e,t){e.__proto__=t}function T(e,t,n){for(var r=0,i=n.length;r<i;r++){var a=n[r];y(e,a,t[a])}}function S(e){if(l(e)){var t;return i(e,"__ob__")&&e.__ob__ instanceof ti?t=e.__ob__:ei.shouldConvert&&!Tr._isServer&&(Array.isArray(e)||f(e))&&Object.isExtensible(e)&&!e._isVue&&(t=new ti(e)),t}}function E(e,t,n,r){var i=new Hr,a=Object.getOwnPropertyDescriptor(e,t);if(!a||a.configurable!==!1){var o=a&&a.get,s=a&&a.set,c=S(n);Object.defineProperty(e,t,{enumerable:!0,configurable:!0,get:function(){var t=o?o.call(e):n;return Hr.target&&(i.depend(),c&&c.dep.depend(),Array.isArray(t)&&N(t)),t},set:function(t){var r=o?o.call(e):n;t!==r&&(s?s.call(e,t):n=t,c=S(t),i.notify())}})}}function j(e,t,n){if(Array.isArray(e))return e.splice(t,1,n),n;if(i(e,t))return void(e[t]=n);var r=e.__ob__;if(!(e._isVue||r&&r.vmCount))return r?(E(r.value,t,n),r.dep.notify(),n):void(e[t]=n)}function L(e,t){var n=e.__ob__;e._isVue||n&&n.vmCount||i(e,t)&&(delete e[t],n&&n.dep.notify())}function N(e){for(var t=void 0,n=0,r=e.length;n<r;n++)t=e[n],t&&t.__ob__&&t.__ob__.dep.depend(),Array.isArray(t)&&N(t)}function D(e){e._watchers=[],M(e),P(e),R(e),B(e),F(e)}function M(e){var t=e.$options.props;if(t){var n=e.$options.propsData||{},r=e.$options._propKeys=Object.keys(t),i=!e.$parent;ei.shouldConvert=i;for(var a=function(i){var a=r[i];E(e,a,Le(a,t,n,e))},o=0;o<r.length;o++)a(o);ei.shouldConvert=!0}}function P(e){var t=e.$options.data;t=e._data="function"==typeof t?t.call(e):t||{},f(t)||(t={});for(var n=Object.keys(t),r=e.$options.props,a=n.length;a--;)r&&i(r,n[a])||z(e,n[a]);S(t),t.__ob__&&t.__ob__.vmCount++}function R(e){var t=e.$options.computed;if(t)for(var n in t){var r=t[n];"function"==typeof r?(ni.get=I(r,e),ni.set=p):(ni.get=r.get?r.cache!==!1?I(r.get,e):s(r.get,e):p,ni.set=r.set?s(r.set,e):p),Object.defineProperty(e,n,ni)}}function I(e,t){var n=new Zr(t,e,p,{lazy:!0});return function(){return n.dirty&&n.evaluate(),Hr.target&&n.depend(),n.value}}function B(e){var t=e.$options.methods;if(t)for(var n in t)e[n]=null==t[n]?p:s(t[n],e)}function F(e){var t=e.$options.watch;if(t)for(var n in t){var r=t[n];if(Array.isArray(r))for(var i=0;i<r.length;i++)H(e,n,r[i]);else H(e,n,r)}}function H(e,t,n){var r;f(n)&&(r=n,n=n.handler),"string"==typeof n&&(n=e[n]),e.$watch(t,n,r)}function U(e){var t={};t.get=function(){return this._data},Object.defineProperty(e.prototype,"$data",t),e.prototype.$set=j,e.prototype.$delete=L,e.prototype.$watch=function(e,t,n){var r=this;n=n||{},n.user=!0;var i=new Zr(r,e,t,n);return n.immediate&&t.call(r,i.value),function(){i.teardown()}}}function z(e,t){g(t)||Object.defineProperty(e,t,{configurable:!0,enumerable:!0,get:function(){return e._data[t]},set:function(n){e._data[t]=n}})}function V(e){var t=new ri(e.tag,e.data,e.children,e.text,e.elm,e.ns,e.context,e.componentOptions);return t.isStatic=e.isStatic,t.key=e.key,t.isCloned=!0,t}function J(e){for(var t=new Array(e.length),n=0;n<e.length;n++)t[n]=V(e[n]);return t}function q(e,t,n,r){r+=t;var i=e.__injected||(e.__injected={});if(!i[r]){i[r]=!0;var a=e[t];a?e[t]=function(){a.apply(this,arguments),n.apply(this,arguments)}:e[t]=n}}function K(e,t,n,r,i){var a,o,s,c,u,l;for(a in e)if(o=e[a],s=t[a],o)if(s){if(o!==s)if(Array.isArray(s)){s.length=o.length;for(var f=0;f<s.length;f++)s[f]=o[f];e[a]=s}else s.fn=o,e[a]=s}else l="!"===a.charAt(0),u=l?a.slice(1):a,Array.isArray(o)?n(u,o.invoker=W(o),l):(o.invoker||(c=o,o=e[a]={},o.fn=c,o.invoker=Z(o)),n(u,o.invoker,l));else;for(a in t)e[a]||(u="!"===a.charAt(0)?a.slice(1):a,r(u,t[a].invoker))}function W(e){return function(t){for(var n=arguments,r=1===arguments.length,i=0;i<e.length;i++)r?e[i](t):e[i].apply(null,n)}}function Z(e){return function(t){var n=1===arguments.length;n?e.fn(t):e.fn.apply(null,arguments)}}function G(e,t,n){if(a(e))return[Y(e)];if(Array.isArray(e)){for(var r=[],i=0,o=e.length;i<o;i++){var s=e[i],c=r[r.length-1];Array.isArray(s)?r.push.apply(r,G(s,t,(n||"")+"_"+i)):a(s)?c&&c.text?c.text+=String(s):""!==s&&r.push(Y(s)):s instanceof ri&&(s.text&&c&&c.text?c.text+=s.text:(t&&Q(s,t),s.tag&&null==s.key&&null!=n&&(s.key="__vlist"+n+"_"+i+"__"),r.push(s)))}return r}}function Y(e){return new ri(void 0,void 0,void 0,String(e))}function Q(e,t){if(e.tag&&!e.ns&&(e.ns=t,e.children))for(var n=0,r=e.children.length;n<r;n++)Q(e.children[n],t)}function X(e){return e&&e.filter(function(e){return e&&e.componentOptions})[0]}function ee(e){var t=e.$options,n=t.parent;if(n&&!t.abstract){for(;n.$options.abstract&&n.$parent;)n=n.$parent;n.$children.push(e)}e.$parent=n,e.$root=n?n.$root:e,e.$children=[],e.$refs={},e._watcher=null,e._inactive=!1,e._isMounted=!1,e._isDestroyed=!1,e._isBeingDestroyed=!1}function te(e){e.prototype._mount=function(e,t){var n=this;return n.$el=e,n.$options.render||(n.$options.render=ii),ne(n,"beforeMount"),n._watcher=new Zr(n,function(){n._update(n._render(),t)},p),t=!1,null==n.$vnode&&(n._isMounted=!0,ne(n,"mounted")),n},e.prototype._update=function(e,t){var n=this;n._isMounted&&ne(n,"beforeUpdate");var r=n.$el,i=ai;ai=n;var a=n._vnode;n._vnode=e,a?n.$el=n.__patch__(a,e):n.$el=n.__patch__(n.$el,e,t),ai=i,r&&(r.__vue__=null),n.$el&&(n.$el.__vue__=n),n.$vnode&&n.$parent&&n.$vnode===n.$parent._vnode&&(n.$parent.$el=n.$el),n._isMounted&&ne(n,"updated")},e.prototype._updateFromParent=function(e,t,n,r){var i=this,a=!(!i.$options._renderChildren&&!r);if(i.$options._parentVnode=n,i.$options._renderChildren=r,e&&i.$options.props){ei.shouldConvert=!1;for(var o=i.$options._propKeys||[],s=0;s<o.length;s++){var c=o[s];i[c]=Le(c,i.$options.props,e,i)}ei.shouldConvert=!0}if(t){var u=i.$options._parentListeners;i.$options._parentListeners=t,i._updateListeners(t,u)}a&&(i.$slots=_e(r,i._renderContext),i.$forceUpdate())},e.prototype.$forceUpdate=function(){var e=this;e._watcher&&e._watcher.update()},e.prototype.$destroy=function(){var e=this;if(!e._isBeingDestroyed){ne(e,"beforeDestroy"),e._isBeingDestroyed=!0;var t=e.$parent;!t||t._isBeingDestroyed||e.$options.abstract||r(t.$children,e),e._watcher&&e._watcher.teardown();for(var n=e._watchers.length;n--;)e._watchers[n].teardown();e._data.__ob__&&e._data.__ob__.vmCount--,e._isDestroyed=!0,ne(e,"destroyed"),e.$off(),e.$el&&(e.$el.__vue__=null),e.__patch__(e._vnode,null)}}}function ne(e,t){var n=e.$options[t];if(n)for(var r=0,i=n.length;r<i;r++)n[r].call(e);e.$emit("hook:"+t)}function re(e,t,n,r,i){if(e&&(l(e)&&(e=Ce.extend(e)),"function"==typeof e)){if(!e.cid)if(e.resolved)e=e.resolved;else if(e=le(e,function(){n.$forceUpdate()}),!e)return;t=t||{};var a=fe(t,e);if(e.options.functional)return ie(e,a,t,n,r);var o=t.on;t.on=t.nativeOn,e.options.abstract&&(t={}),pe(t);var s=e.options.name||i,c=new ri("vue-component-"+e.cid+(s?"-"+s:""),t,void 0,void 0,void 0,void 0,n,{Ctor:e,propsData:a,listeners:o,tag:i,children:r});return c}}function ie(e,t,n,r,i){var a={},o=e.options.props;if(o)for(var c in o)a[c]=Le(c,o,t);var u=e.options.render.call(null,s(he,{_self:Object.create(r)}),{props:a,data:n,parent:r,children:G(i),slots:function(){return _e(i,r)}});return u instanceof ri&&(u.functionalContext=r,n.slot&&((u.data||(u.data={})).slot=n.slot)),u}function ae(e,t){var n=e.componentOptions,r={_isComponent:!0,parent:t,propsData:n.propsData,_componentTag:n.tag,_parentVnode:e,_parentListeners:n.listeners,_renderChildren:n.children},i=e.data.inlineTemplate;return i&&(r.render=i.render,r.staticRenderFns=i.staticRenderFns),new n.Ctor(r)}function oe(e,t){if(!e.child||e.child._isDestroyed){var n=e.child=ae(e,ai);n.$mount(t?e.elm:void 0,t)}}function se(e,t){var n=t.componentOptions,r=t.child=e.child;r._updateFromParent(n.propsData,n.listeners,t,n.children)}function ce(e){e.child._isMounted||(e.child._isMounted=!0,ne(e.child,"mounted")),e.data.keepAlive&&(e.child._inactive=!1,ne(e.child,"activated"))}function ue(e){e.child._isDestroyed||(e.data.keepAlive?(e.child._inactive=!0,ne(e.child,"deactivated")):e.child.$destroy())}function le(e,t){if(!e.requested){e.requested=!0;var n=e.pendingCallbacks=[t],r=!0,i=function(t){if(l(t)&&(t=Ce.extend(t)),e.resolved=t,!r)for(var i=0,a=n.length;i<a;i++)n[i](t)},a=function(e){},o=e(i,a);return o&&"function"==typeof o.then&&!e.resolved&&o.then(i,a),r=!1,e.resolved}e.pendingCallbacks.push(t)}function fe(e,t){var n=t.options.props;if(n){var r={},i=e.attrs,a=e.props,o=e.domProps;if(i||a||o)for(var s in n){var c=xr(s);de(r,a,s,c,!0)||de(r,i,s,c)||de(r,o,s,c)}return r}}function de(e,t,n,r,a){if(t){if(i(t,n))return e[n]=t[n],a||delete t[n],!0;if(i(t,r))return e[n]=t[r],a||delete t[r],!0}return!1}function pe(e){e.hook||(e.hook={});for(var t=0;t<si.length;t++){var n=si[t],r=e.hook[n],i=oi[n];e.hook[n]=r?ve(i,r):i}}function ve(e,t){return function(n,r){e(n,r),t(n,r)}}function he(e,t,n){return t&&(Array.isArray(t)||"object"!=typeof t)&&(n=t,t=void 0),me(this._self,e,t,n)}function me(e,t,n,r){if(!n||!n.__ob__){if(!t)return ii();if("string"==typeof t){var i,a=Tr.getTagNamespace(t);return Tr.isReservedTag(t)?new ri(t,n,G(r,a),void 0,void 0,a,e):(i=je(e.$options,"components",t))?re(i,n,e,r,t):new ri(t,n,G(r,a),void 0,void 0,a,e)}return re(t,n,e,r)}}function ge(e){e.$vnode=null,e._vnode=null,e._staticTrees=null,e._renderContext=e.$options._parentVnode&&e.$options._parentVnode.context,e.$slots=_e(e.$options._renderChildren,e._renderContext),e.$createElement=s(he,e),e.$options.el&&e.$mount(e.$options.el)}function ye(n){n.prototype.$nextTick=function(e){Br(e,this)},n.prototype._render=function(){var e=this,t=e.$options,n=t.render,r=t.staticRenderFns,i=t._parentVnode;if(e._isMounted)for(var a in e.$slots)e.$slots[a]=J(e.$slots[a]);r&&!e._staticTrees&&(e._staticTrees=[]),e.$vnode=i;var o;try{o=n.call(e._renderProxy,e.$createElement)}catch(t){if(Tr.errorHandler)Tr.errorHandler.call(null,t,e);else{if(Tr._isServer)throw t;setTimeout(function(){throw t},0)}o=e._vnode}return o instanceof ri||(o=ii()),o.parent=i,o},n.prototype._h=he,n.prototype._s=e,n.prototype._n=t,n.prototype._e=ii,n.prototype._q=h,n.prototype._i=m,n.prototype._m=function(e,t){var n=this._staticTrees[e];if(n&&!t)return Array.isArray(n)?J(n):V(n);if(n=this._staticTrees[e]=this.$options.staticRenderFns[e].call(this._renderProxy),Array.isArray(n))for(var r=0;r<n.length;r++)"string"!=typeof n[r]&&(n[r].isStatic=!0,n[r].key="__static__"+e+"_"+r);else n.isStatic=!0,n.key="__static__"+e;return n};var r=function(e){return e};n.prototype._f=function(e){return je(this.$options,"filters",e,!0)||r},n.prototype._l=function(e,t){var n,r,i,a,o;if(Array.isArray(e))for(n=new Array(e.length),r=0,i=e.length;r<i;r++)n[r]=t(e[r],r);else if("number"==typeof e)for(n=new Array(e),r=0;r<e;r++)n[r]=t(r+1,r);else if(l(e))for(a=Object.keys(e),n=new Array(a.length),r=0,i=a.length;r<i;r++)o=a[r],n[r]=t(e[o],o,r);return n},n.prototype._t=function(e,t){var n=this.$slots[e];return n||t},n.prototype._b=function(e,t,n){if(t)if(l(t)){Array.isArray(t)&&(t=d(t));for(var r in t)if("class"===r||"style"===r)e[r]=t[r];else{var i=n||Tr.mustUseProp(r)?e.domProps||(e.domProps={}):e.attrs||(e.attrs={});i[r]=t[r]}}else;return e},n.prototype._k=function(e){return Tr.keyCodes[e]}}function _e(e,t){var n={};if(!e)return n;for(var r,i,a=G(e)||[],o=[],s=0,c=a.length;s<c;s++)if(i=a[s],(i.context===t||i.functionalContext===t)&&i.data&&(r=i.data.slot)){var u=n[r]||(n[r]=[]);"template"===i.tag?u.push.apply(u,i.children):u.push(i)}else o.push(i);return o.length&&(1!==o.length||" "!==o[0].text&&!o[0].isComment)&&(n.default=o),n}function be(e){e._events=Object.create(null);var t=e.$options._parentListeners,n=s(e.$on,e),r=s(e.$off,e);e._updateListeners=function(t,i){K(t,i||{},n,r,e)},t&&e._updateListeners(t)}function $e(e){e.prototype.$on=function(e,t){var n=this;return(n._events[e]||(n._events[e]=[])).push(t),n},e.prototype.$once=function(e,t){function n(){r.$off(e,n),t.apply(r,arguments)}var r=this;return n.fn=t,r.$on(e,n),r},e.prototype.$off=function(e,t){var n=this;if(!arguments.length)return n._events=Object.create(null),n;var r=n._events[e];if(!r)return n;if(1===arguments.length)return n._events[e]=null,n;for(var i,a=r.length;a--;)if(i=r[a],i===t||i.fn===t){r.splice(a,1);break}return n},e.prototype.$emit=function(e){var t=this,n=t._events[e];if(n){n=n.length>1?c(n):n;for(var r=c(arguments,1),i=0,a=n.length;i<a;i++)n[i].apply(t,r)}return t}}function we(e){function t(e,t){var r=e.$options=Object.create(n(e));r.parent=t.parent,r.propsData=t.propsData,r._parentVnode=t._parentVnode,r._parentListeners=t._parentListeners,r._renderChildren=t._renderChildren,r._componentTag=t._componentTag,t.render&&(r.render=t.render,r.staticRenderFns=t.staticRenderFns)}function n(e){var t=e.constructor,n=t.options;if(t.super){var r=t.super.options,i=t.superOptions;r!==i&&(t.superOptions=r,n=t.options=Ee(r,t.extendOptions),n.name&&(n.components[n.name]=t))}return n}e.prototype._init=function(e){var r=this;r._uid=ci++,r._isVue=!0,e&&e._isComponent?t(r,e):r.$options=Ee(n(r),e||{},r),r._renderProxy=r,r._self=r,ee(r),be(r),ne(r,"beforeCreate"),D(r),ne(r,"created"),ge(r)}}function Ce(e){this._init(e)}function xe(e,t){var n,r,a;for(n in t)r=e[n],a=t[n],i(e,n)?l(r)&&l(a)&&xe(r,a):j(e,n,a);return e}function ke(e,t){return t?e?e.concat(t):Array.isArray(t)?t:[t]:e}function Ae(e,t){var n=Object.create(e||null);return t?u(n,t):n}function Oe(e){if(e.components){var t,n=e.components;for(var r in n){var i=r.toLowerCase();yr(i)||Tr.isReservedTag(i)||(t=n[r],f(t)&&(n[r]=Ce.extend(t)))}}}function Te(e){var t=e.props;if(t){var n,r,i,a={};if(Array.isArray(t))for(n=t.length;n--;)r=t[n],"string"==typeof r&&(i=$r(r),a[i]={type:null});else if(f(t))for(var o in t)r=t[o],i=$r(o),a[i]=f(r)?r:{type:r};e.props=a}}function Se(e){var t=e.directives;if(t)for(var n in t){var r=t[n];"function"==typeof r&&(t[n]={bind:r,update:r})}}function Ee(e,t,n){function r(r){var i=fi[r]||di;l[r]=i(e[r],t[r],n,r)}Oe(t),Te(t),Se(t);var a=t.extends;if(a&&(e="function"==typeof a?Ee(e,a.options,n):Ee(e,a,n)),t.mixins)for(var o=0,s=t.mixins.length;o<s;o++){var c=t.mixins[o];c.prototype instanceof Ce&&(c=c.options),e=Ee(e,c,n)}var u,l={};for(u in e)r(u);for(u in t)i(e,u)||r(u);return l}function je(e,t,n,r){if("string"==typeof n){var i=e[t],a=i[n]||i[$r(n)]||i[wr($r(n))];return a}}function Le(e,t,n,r){var a=t[e],o=!i(n,e),s=n[e];if(Me(a.type)&&(o&&!i(a,"default")?s=!1:""!==s&&s!==xr(e)||(s=!0)),void 0===s){s=Ne(r,a,e);var c=ei.shouldConvert;ei.shouldConvert=!0,S(s),ei.shouldConvert=c}return s}function Ne(e,t,n){if(i(t,"default")){var r=t.default;return l(r),"function"==typeof r&&t.type!==Function?r.call(e):r}}function De(e){var t=e&&e.toString().match(/^\s*function (\w+)/);return t&&t[1]}function Me(e){if(!Array.isArray(e))return"Boolean"===De(e);for(var t=0,n=e.length;t<n;t++)if("Boolean"===De(e[t]))return!0;return!1}function Pe(e){e.use=function(e){if(!e.installed){var t=c(arguments,1);return t.unshift(this),"function"==typeof e.install?e.install.apply(e,t):e.apply(null,t),e.installed=!0,this}}}function Re(e){e.mixin=function(t){e.options=Ee(e.options,t)}}function Ie(e){e.cid=0;var t=1;e.extend=function(e){e=e||{};var n=this,r=0===n.cid;if(r&&e._Ctor)return e._Ctor;var i=e.name||n.options.name,a=function(e){this._init(e)};return a.prototype=Object.create(n.prototype),a.prototype.constructor=a,a.cid=t++,a.options=Ee(n.options,e),a.super=n,a.extend=n.extend,Tr._assetTypes.forEach(function(e){a[e]=n[e]}),i&&(a.options.components[i]=a),a.superOptions=n.options,a.extendOptions=e,r&&(e._Ctor=a),a}}function Be(e){Tr._assetTypes.forEach(function(t){e[t]=function(n,r){return r?("component"===t&&f(r)&&(r.name=r.name||n,r=e.extend(r)),"directive"===t&&"function"==typeof r&&(r={bind:r,update:r}),this.options[t+"s"][n]=r,r):this.options[t+"s"][n]}})}function Fe(e){var t={};t.get=function(){return Tr},Object.defineProperty(e,"config",t),e.util=pi,e.set=j,e.delete=L,e.nextTick=Br,e.options=Object.create(null),Tr._assetTypes.forEach(function(t){e.options[t+"s"]=Object.create(null)}),u(e.options.components,hi),Pe(e),Re(e),Ie(e),Be(e)}function He(e){for(var t=e.data,n=e,r=e;r.child;)r=r.child._vnode,r.data&&(t=Ue(r.data,t));for(;n=n.parent;)n.data&&(t=Ue(t,n.data));return ze(t)}function Ue(e,t){return{staticClass:Ve(e.staticClass,t.staticClass),class:e.class?[e.class,t.class]:t.class}}function ze(e){var t=e.class,n=e.staticClass;return n||t?Ve(n,Je(t)):""}function Ve(e,t){return e?t?e+" "+t:e:t||""}function Je(e){var t="";if(!e)return t;if("string"==typeof e)return e;if(Array.isArray(e)){for(var n,r=0,i=e.length;r<i;r++)e[r]&&(n=Je(e[r]))&&(t+=n+" ");return t.slice(0,-1)}if(l(e)){for(var a in e)e[a]&&(t+=a+" ");return t.slice(0,-1)}return t}function qe(e){return Si(e)?"svg":"math"===e?"math":void 0}function Ke(e){if(!jr)return!0;if(ji(e))return!1;if(e=e.toLowerCase(),null!=Li[e])return Li[e];var t=document.createElement(e);return e.indexOf("-")>-1?Li[e]=t.constructor===window.HTMLUnknownElement||t.constructor===window.HTMLElement:Li[e]=/HTMLUnknownElement/.test(t.toString())}function We(e){if("string"==typeof e){if(e=document.querySelector(e),!e)return document.createElement("div")}return e}function Ze(e,t){var n=document.createElement(e);return"select"!==e?n:(t.data&&t.data.attrs&&"multiple"in t.data.attrs&&n.setAttribute("multiple","multiple"),n)}function Ge(e,t){return document.createElementNS(xi[e],t)}function Ye(e){return document.createTextNode(e)}function Qe(e){return document.createComment(e)}function Xe(e,t,n){e.insertBefore(t,n)}function et(e,t){e.removeChild(t)}function tt(e,t){e.appendChild(t)}function nt(e){return e.parentNode}function rt(e){return e.nextSibling}function it(e){return e.tagName}function at(e,t){e.textContent=t}function ot(e){return e.childNodes}function st(e,t,n){e.setAttribute(t,n)}function ct(e,t){var n=e.data.ref;if(n){var i=e.context,a=e.child||e.elm,o=i.$refs;t?Array.isArray(o[n])?r(o[n],a):o[n]===a&&(o[n]=void 0):e.data.refInFor?Array.isArray(o[n])?o[n].push(a):o[n]=[a]:o[n]=a}}function ut(e){return null==e}function lt(e){return null!=e}function ft(e,t){return e.key===t.key&&e.tag===t.tag&&e.isComment===t.isComment&&!e.data==!t.data}function dt(e,t,n){var r,i,a={};for(r=t;r<=n;++r)i=e[r].key,lt(i)&&(a[i]=r);return a}function pt(e){function t(e){return new ri(C.tagName(e).toLowerCase(),{},[],void 0,e)}function n(e,t){function n(){0===--n.listeners&&r(e)}return n.listeners=t,n}function r(e){var t=C.parentNode(e);C.removeChild(t,e)}function i(e,t,n){var r,i=e.data;if(e.isRootInsert=!n,lt(i)&&(lt(r=i.hook)&&lt(r=r.init)&&r(e),lt(r=e.child)))return u(e,t),e.elm;var a=e.children,s=e.tag;return lt(s)?(e.elm=e.ns?C.createElementNS(e.ns,s):C.createElement(s,e),l(e),o(e,a,t),lt(i)&&c(e,t)):e.isComment?e.elm=C.createComment(e.text):e.elm=C.createTextNode(e.text),e.elm}function o(e,t,n){if(Array.isArray(t))for(var r=0;r<t.length;++r)C.appendChild(e.elm,i(t[r],n,!0));else a(e.text)&&C.appendChild(e.elm,C.createTextNode(e.text))}function s(e){for(;e.child;)e=e.child._vnode;return lt(e.tag)}function c(e,t){for(var n=0;n<$.create.length;++n)$.create[n](Mi,e);_=e.data.hook,lt(_)&&(_.create&&_.create(Mi,e),_.insert&&t.push(e))}function u(e,t){e.data.pendingInsert&&t.push.apply(t,e.data.pendingInsert),e.elm=e.child.$el,s(e)?(c(e,t),l(e)):(ct(e),t.push(e))}function l(e){var t;lt(t=e.context)&&lt(t=t.$options._scopeId)&&C.setAttribute(e.elm,t,""),lt(t=ai)&&t!==e.context&&lt(t=t.$options._scopeId)&&C.setAttribute(e.elm,t,"")}function f(e,t,n,r,a,o){for(;r<=a;++r)C.insertBefore(e,i(n[r],o),t)}function d(e){var t,n,r=e.data;if(lt(r))for(lt(t=r.hook)&&lt(t=t.destroy)&&t(e),t=0;t<$.destroy.length;++t)$.destroy[t](e);if(lt(t=e.children))for(n=0;n<e.children.length;++n)d(e.children[n])}function p(e,t,n,r){for(;n<=r;++n){var i=t[n];lt(i)&&(lt(i.tag)?(v(i),d(i)):C.removeChild(e,i.elm))}}function v(e,t){if(t||lt(e.data)){var i=$.remove.length+1;for(t?t.listeners+=i:t=n(e.elm,i),lt(_=e.child)&&lt(_=_._vnode)&&lt(_.data)&&v(_,t),_=0;_<$.remove.length;++_)$.remove[_](e,t);lt(_=e.data.hook)&&lt(_=_.remove)?_(e,t):t()}else r(e.elm)}function h(e,t,n,r,a){for(var o,s,c,u,l=0,d=0,v=t.length-1,h=t[0],g=t[v],y=n.length-1,_=n[0],b=n[y],$=!a;l<=v&&d<=y;)ut(h)?h=t[++l]:ut(g)?g=t[--v]:ft(h,_)?(m(h,_,r),h=t[++l],_=n[++d]):ft(g,b)?(m(g,b,r),g=t[--v],b=n[--y]):ft(h,b)?(m(h,b,r),$&&C.insertBefore(e,h.elm,C.nextSibling(g.elm)),h=t[++l],b=n[--y]):ft(g,_)?(m(g,_,r),$&&C.insertBefore(e,g.elm,h.elm),g=t[--v],_=n[++d]):(ut(o)&&(o=dt(t,l,v)),s=lt(_.key)?o[_.key]:null,ut(s)?(C.insertBefore(e,i(_,r),h.elm),_=n[++d]):(c=t[s],c.tag!==_.tag?(C.insertBefore(e,i(_,r),h.elm),_=n[++d]):(m(c,_,r),t[s]=void 0,$&&C.insertBefore(e,_.elm,h.elm),_=n[++d])));l>v?(u=ut(n[y+1])?null:n[y+1].elm,f(e,u,n,d,y,r)):d>y&&p(e,t,l,v)}function m(e,t,n,r){if(e!==t){if(t.isStatic&&e.isStatic&&t.key===e.key&&t.isCloned)return void(t.elm=e.elm);var i,a=t.data,o=lt(a);o&&lt(i=a.hook)&&lt(i=i.prepatch)&&i(e,t);var c=t.elm=e.elm,u=e.children,l=t.children;if(o&&s(t)){for(i=0;i<$.update.length;++i)$.update[i](e,t);lt(i=a.hook)&&lt(i=i.update)&&i(e,t)}ut(t.text)?lt(u)&&lt(l)?u!==l&&h(c,u,l,n,r):lt(l)?(lt(e.text)&&C.setTextContent(c,""),f(c,null,l,0,l.length-1,n)):lt(u)?p(c,u,0,u.length-1):lt(e.text)&&C.setTextContent(c,""):e.text!==t.text&&C.setTextContent(c,t.text),o&&lt(i=a.hook)&&lt(i=i.postpatch)&&i(e,t)}}function g(e,t,n){if(n&&e.parent)e.parent.data.pendingInsert=t;else for(var r=0;r<t.length;++r)t[r].data.hook.insert(t[r])}function y(e,t,n){t.elm=e;var r=t.tag,i=t.data,a=t.children;if(lt(i)&&(lt(_=i.hook)&&lt(_=_.init)&&_(t,!0),lt(_=t.child)))return u(t,n),!0;if(lt(r)){if(lt(a)){var s=C.childNodes(e);if(s.length){var l=!0;if(s.length!==a.length)l=!1;else for(var f=0;f<a.length;f++)if(!y(s[f],a[f],n)){l=!1;break}if(!l)return!1}else o(t,a,n)}lt(i)&&c(t,n)}return!0}var _,b,$={},w=e.modules,C=e.nodeOps;for(_=0;_<Pi.length;++_)for($[Pi[_]]=[],b=0;b<w.length;++b)void 0!==w[b][Pi[_]]&&$[Pi[_]].push(w[b][Pi[_]]);return function(e,n,r,a){if(!n)return void(e&&d(e));var o,c,u=!1,l=[];if(e){var f=lt(e.nodeType);if(!f&&ft(e,n))m(e,n,l,a);else{if(f){if(1===e.nodeType&&e.hasAttribute("server-rendered")&&(e.removeAttribute("server-rendered"),r=!0),r&&y(e,n,l))return g(n,l,!0),e;e=t(e)}if(o=e.elm,c=C.parentNode(o),i(n,l),n.parent&&(n.parent.elm=n.elm,s(n)))for(var v=0;v<$.create.length;++v)$.create[v](Mi,n.parent);null!==c?(C.insertBefore(c,n.elm,C.nextSibling(o)),p(c,[e],0,0)):lt(e.tag)&&d(e)}}else u=!0,i(n,l);return g(n,l,u),n.elm}}function vt(e,t){if(e.data.directives||t.data.directives){var n,r,i,a=e===Mi,o=ht(e.data.directives,e.context),s=ht(t.data.directives,t.context),c=[],u=[];for(n in s)r=o[n],i=s[n],r?(i.oldValue=r.value,gt(i,"update",t,e),i.def&&i.def.componentUpdated&&u.push(i)):(gt(i,"bind",t,e),i.def&&i.def.inserted&&c.push(i));if(c.length){var l=function(){c.forEach(function(n){gt(n,"inserted",t,e)})};a?q(t.data.hook||(t.data.hook={}),"insert",l,"dir-insert"):l()}if(u.length&&q(t.data.hook||(t.data.hook={}),"postpatch",function(){u.forEach(function(n){gt(n,"componentUpdated",t,e)})},"dir-postpatch"),!a)for(n in o)s[n]||gt(o[n],"unbind",e)}}function ht(e,t){var n=Object.create(null);if(!e)return n;var r,i;for(r=0;r<e.length;r++)i=e[r],i.modifiers||(i.modifiers=Ii),n[mt(i)]=i,i.def=je(t.$options,"directives",i.name,!0);return n}function mt(e){return e.rawName||e.name+"."+Object.keys(e.modifiers||{}).join(".")}function gt(e,t,n,r){var i=e.def&&e.def[t];i&&i(n.elm,e,n,r)}function yt(e,t){if(e.data.attrs||t.data.attrs){var n,r,i,a=t.elm,o=e.data.attrs||{},s=t.data.attrs||{};s.__ob__&&(s=t.data.attrs=u({},s));for(n in s)r=s[n],i=o[n],i!==r&&_t(a,n,r);for(n in o)null==s[n]&&($i(n)?a.removeAttributeNS(bi,wi(n)):yi(n)||a.removeAttribute(n))}}function _t(e,t,n){_i(t)?Ci(n)?e.removeAttribute(t):e.setAttribute(t,t):yi(t)?e.setAttribute(t,Ci(n)||"false"===n?"false":"true"):$i(t)?Ci(n)?e.removeAttributeNS(bi,wi(t)):e.setAttributeNS(bi,t,n):Ci(n)?e.removeAttribute(t):e.setAttribute(t,n)}function bt(e,t){var n=t.elm,r=t.data,i=e.data;if(r.staticClass||r.class||i&&(i.staticClass||i.class)){var a=He(t),o=n._transitionClasses;o&&(a=Ve(a,Je(o))),a!==n._prevClass&&(n.setAttribute("class",a),n._prevClass=a)}}function $t(e,t){if(e.data.on||t.data.on){var n=t.data.on||{},r=e.data.on||{},i=t.elm._v_add||(t.elm._v_add=function(e,n,r){t.elm.addEventListener(e,n,r)}),a=t.elm._v_remove||(t.elm._v_remove=function(e,n){t.elm.removeEventListener(e,n)});K(n,r,i,a,t.context)}}function wt(e,t){if(e.data.domProps||t.data.domProps){var n,r,i=t.elm,a=e.data.domProps||{},o=t.data.domProps||{};o.__ob__&&(o=t.data.domProps=u({},o));for(n in a)null==o[n]&&(i[n]=void 0);for(n in o)if("textContent"!==n&&"innerHTML"!==n||!t.children||(t.children.length=0),r=o[n],"value"===n){i._value=r;var s=null==r?"":String(r);i.value===s||i.composing||(i.value=s)}else i[n]=r}}function Ct(e,t){if(e.data&&e.data.style||t.data.style){var n,r,i=t.elm,a=e.data.style||{},o=t.data.style||{};if("string"==typeof o)return void(i.style.cssText=o);var s=o.__ob__;Array.isArray(o)&&(o=t.data.style=d(o)),s&&(o=t.data.style=u({},o));for(r in a)null==o[r]&&(i.style[Ji(r)]="");for(r in o)n=o[r],n!==a[r]&&(i.style[Ji(r)]=null==n?"":n)}}function xt(e,t){if(e.classList)t.indexOf(" ")>-1?t.split(/\s+/).forEach(function(t){return e.classList.add(t)}):e.classList.add(t);else{var n=" "+e.getAttribute("class")+" ";n.indexOf(" "+t+" ")<0&&e.setAttribute("class",(n+t).trim())}}function kt(e,t){if(e.classList)t.indexOf(" ")>-1?t.split(/\s+/).forEach(function(t){return e.classList.remove(t)}):e.classList.remove(t);else{for(var n=" "+e.getAttribute("class")+" ",r=" "+t+" ";n.indexOf(r)>=0;)n=n.replace(r," ");e.setAttribute("class",n.trim())}}function At(e){ea(function(){ea(e)})}function Ot(e,t){(e._transitionClasses||(e._transitionClasses=[])).push(t),xt(e,t)}function Tt(e,t){e._transitionClasses&&r(e._transitionClasses,t),kt(e,t)}function St(e,t,n){var r=Et(e,t),i=r.type,a=r.timeout,o=r.propCount;if(!i)return n();var s=i===Wi?Yi:Xi,c=0,u=function(){e.removeEventListener(s,l),n()},l=function(t){t.target===e&&++c>=o&&u()};setTimeout(function(){c<o&&u()},a+1),e.addEventListener(s,l)}function Et(e,t){var n,r=window.getComputedStyle(e),i=r[Gi+"Delay"].split(", "),a=r[Gi+"Duration"].split(", "),o=jt(i,a),s=r[Qi+"Delay"].split(", "),c=r[Qi+"Duration"].split(", "),u=jt(s,c),l=0,f=0;t===Wi?o>0&&(n=Wi,l=o,f=a.length):t===Zi?u>0&&(n=Zi,l=u,f=c.length):(l=Math.max(o,u),n=l>0?o>u?Wi:Zi:null,f=n?n===Wi?a.length:c.length:0);var d=n===Wi&&ta.test(r[Gi+"Property"]);return{type:n,timeout:l,propCount:f,hasTransform:d}}function jt(e,t){return Math.max.apply(null,t.map(function(t,n){return Lt(t)+Lt(e[n])}))}function Lt(e){return 1e3*Number(e.slice(0,-1))}function Nt(e){var t=e.elm;t._leaveCb&&(t._leaveCb.cancelled=!0,t._leaveCb());var n=Mt(e.data.transition);if(n&&!t._enterCb&&1===t.nodeType){var r=n.css,i=n.type,a=n.enterClass,o=n.enterActiveClass,s=n.appearClass,c=n.appearActiveClass,u=n.beforeEnter,l=n.enter,f=n.afterEnter,d=n.enterCancelled,p=n.beforeAppear,v=n.appear,h=n.afterAppear,m=n.appearCancelled,g=ai.$vnode,y=g&&g.parent?g.parent.context:ai,_=!y._isMounted||!e.isRootInsert;if(!_||v||""===v){var b=_?s:a,$=_?c:o,w=_?p||u:u,C=_&&"function"==typeof v?v:l,x=_?h||f:f,k=_?m||d:d,A=r!==!1&&!Dr,O=C&&(C._length||C.length)>1,T=t._enterCb=Pt(function(){A&&Tt(t,$),T.cancelled?(A&&Tt(t,b),k&&k(t)):x&&x(t),t._enterCb=null});e.data.show||q(e.data.hook||(e.data.hook={}),"insert",function(){var n=t.parentNode,r=n&&n._pending&&n._pending[e.key];r&&r.tag===e.tag&&r.elm._leaveCb&&r.elm._leaveCb(),C&&C(t,T)},"transition-insert"),w&&w(t),A&&(Ot(t,b),Ot(t,$),At(function(){Tt(t,b),T.cancelled||O||St(t,i,T)})),e.data.show&&C&&C(t,T),A||O||T()}}}function Dt(e,t){function n(){m.cancelled||(e.data.show||((r.parentNode._pending||(r.parentNode._pending={}))[e.key]=e),u&&u(r),v&&(Ot(r,s),Ot(r,c),At(function(){Tt(r,s),m.cancelled||h||St(r,o,m)})),l&&l(r,m),v||h||m())}var r=e.elm;r._enterCb&&(r._enterCb.cancelled=!0,r._enterCb());var i=Mt(e.data.transition);if(!i)return t();if(!r._leaveCb&&1===r.nodeType){var a=i.css,o=i.type,s=i.leaveClass,c=i.leaveActiveClass,u=i.beforeLeave,l=i.leave,f=i.afterLeave,d=i.leaveCancelled,p=i.delayLeave,v=a!==!1&&!Dr,h=l&&(l._length||l.length)>1,m=r._leaveCb=Pt(function(){r.parentNode&&r.parentNode._pending&&(r.parentNode._pending[e.key]=null),v&&Tt(r,c),m.cancelled?(v&&Tt(r,s),d&&d(r)):(t(),f&&f(r)),r._leaveCb=null});p?p(n):n()}}function Mt(e){if(e){if("object"==typeof e){var t={};return e.css!==!1&&u(t,na(e.name||"v")),u(t,e),t}return"string"==typeof e?na(e):void 0}}function Pt(e){var t=!1;return function(){t||(t=!0,e())}}function Rt(e,t,n){var r=t.value,i=e.multiple;if(!i||Array.isArray(r)){for(var a,o,s=0,c=e.options.length;s<c;s++)if(o=e.options[s],i)a=m(r,Bt(o))>-1,o.selected!==a&&(o.selected=a);else if(h(Bt(o),r))return void(e.selectedIndex!==s&&(e.selectedIndex=s));i||(e.selectedIndex=-1)}}function It(e,t){for(var n=0,r=t.length;n<r;n++)if(h(Bt(t[n]),e))return!1;return!0}function Bt(e){return"_value"in e?e._value:e.value}function Ft(e){e.target.composing=!0}function Ht(e){e.target.composing=!1,Ut(e.target,"input")}function Ut(e,t){var n=document.createEvent("HTMLEvents");n.initEvent(t,!0,!0),e.dispatchEvent(n)}function zt(e){return!e.child||e.data&&e.data.transition?e:zt(e.child._vnode)}function Vt(e){var t=e&&e.componentOptions;return t&&t.Ctor.options.abstract?Vt(X(t.children)):e}function Jt(e){var t={},n=e.$options;for(var r in n.propsData)t[r]=e[r];var i=n._parentListeners;for(var a in i)t[$r(a)]=i[a].fn;return t}function qt(e,t){return/\d-keep-alive$/.test(t.tag)?e("keep-alive"):null}function Kt(e){for(;e=e.parent;)if(e.data.transition)return!0}function Wt(e){e.elm._moveCb&&e.elm._moveCb(),e.elm._enterCb&&e.elm._enterCb();
+}function Zt(e){e.data.newPos=e.elm.getBoundingClientRect()}function Gt(e){var t=e.data.pos,n=e.data.newPos,r=t.left-n.left,i=t.top-n.top;if(r||i){e.data.moved=!0;var a=e.elm.style;a.transform=a.WebkitTransform="translate("+r+"px,"+i+"px)",a.transitionDuration="0s"}}function Yt(e,t){var n=document.createElement("div");return n.innerHTML='<div a="'+e+'">',n.innerHTML.indexOf(t)>0}function Qt(e){return ma.innerHTML=e,ma.textContent}function Xt(e,t){return t&&(e=e.replace(Za,"\n")),e.replace(Ka,"<").replace(Wa,">").replace(Ga,"&").replace(Ya,'"')}function en(e,t){function n(t){f+=t,e=e.substring(t)}function r(){var t=e.match(Ca);if(t){var r={tagName:t[1],attrs:[],start:f};n(t[0].length);for(var i,a;!(i=e.match(xa))&&(a=e.match(ba));)n(a[0].length),r.attrs.push(a);if(i)return r.unarySlash=i[1],n(i[0].length),r.end=f,r}}function i(e){var n=e.tagName,r=e.unarySlash;u&&("p"===s&&Ti(n)&&a("",s),Oi(n)&&s===n&&a("",n));for(var i=l(n)||"html"===n&&"head"===s||!!r,o=e.attrs.length,f=new Array(o),d=0;d<o;d++){var p=e.attrs[d];Oa&&p[0].indexOf('""')===-1&&(""===p[3]&&delete p[3],""===p[4]&&delete p[4],""===p[5]&&delete p[5]);var v=p[3]||p[4]||p[5]||"";f[d]={name:p[1],value:Xt(v,t.shouldDecodeNewlines)}}i||(c.push({tag:n,attrs:f}),s=n,r=""),t.start&&t.start(n,f,i,e.start,e.end)}function a(e,n,r,i){var a;if(null==r&&(r=f),null==i&&(i=f),n){var o=n.toLowerCase();for(a=c.length-1;a>=0&&c[a].tag.toLowerCase()!==o;a--);}else a=0;if(a>=0){for(var u=c.length-1;u>=a;u--)t.end&&t.end(c[u].tag,r,i);c.length=a,s=a&&c[a-1].tag}else"br"===n.toLowerCase()?t.start&&t.start(n,[],!0,r,i):"p"===n.toLowerCase()&&(t.start&&t.start(n,[],!1,r,i),t.end&&t.end(n,r,i))}for(var o,s,c=[],u=t.expectHTML,l=t.isUnaryTag||Or,f=0;e;){if(o=e,s&&Ja(s)){var d=s.toLowerCase(),p=qa[d]||(qa[d]=new RegExp("([\\s\\S]*?)(</"+d+"[^>]*>)","i")),v=0,h=e.replace(p,function(e,n,r){return v=r.length,"script"!==d&&"style"!==d&&"noscript"!==d&&(n=n.replace(/<!--([\s\S]*?)-->/g,"$1").replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g,"$1")),t.chars&&t.chars(n),""});f+=e.length-h.length,e=h,a("</"+d+">",d,f-v,f)}else{var m=e.indexOf("<");if(0===m){if(/^<!--/.test(e)){var g=e.indexOf("-->");if(g>=0){n(g+3);continue}}if(/^<!\[/.test(e)){var y=e.indexOf("]>");if(y>=0){n(y+2);continue}}var _=e.match(Aa);if(_){n(_[0].length);continue}var b=e.match(ka);if(b){var $=f;n(b[0].length),a(b[0],b[1],$,f);continue}var w=r();if(w){i(w);continue}}var C=void 0;m>=0?(C=e.substring(0,m),n(m)):(C=e,e=""),t.chars&&t.chars(C)}if(e===o)throw new Error("Error parsing template:\n\n"+e)}a()}function tn(e){function t(){(o||(o=[])).push(e.slice(d,i).trim()),d=i+1}var n,r,i,a,o,s=!1,c=!1,u=0,l=0,f=0,d=0;for(i=0;i<e.length;i++)if(r=n,n=e.charCodeAt(i),s)39===n&&92!==r&&(s=!s);else if(c)34===n&&92!==r&&(c=!c);else if(124!==n||124===e.charCodeAt(i+1)||124===e.charCodeAt(i-1)||u||l||f)switch(n){case 34:c=!0;break;case 39:s=!0;break;case 40:f++;break;case 41:f--;break;case 91:l++;break;case 93:l--;break;case 123:u++;break;case 125:u--}else void 0===a?(d=i+1,a=e.slice(0,i).trim()):t();if(void 0===a?a=e.slice(0,i).trim():0!==d&&t(),o)for(i=0;i<o.length;i++)a=nn(a,o[i]);return a}function nn(e,t){var n=t.indexOf("(");if(n<0)return'_f("'+t+'")('+e+")";var r=t.slice(0,n),i=t.slice(n+1);return'_f("'+r+'")('+e+","+i}function rn(e,t){var n=t?eo(t):Qa;if(n.test(e)){for(var r,i,a=[],o=n.lastIndex=0;r=n.exec(e);){i=r.index,i>o&&a.push(JSON.stringify(e.slice(o,i)));var s=tn(r[1].trim());a.push("_s("+s+")"),o=i+r[0].length}return o<e.length&&a.push(JSON.stringify(e.slice(o))),a.join("+")}}function an(e){console.error("[Vue parser]: "+e)}function on(e,t){return e?e.map(function(e){return e[t]}).filter(function(e){return e}):[]}function sn(e,t,n){(e.props||(e.props=[])).push({name:t,value:n})}function cn(e,t,n){(e.attrs||(e.attrs=[])).push({name:t,value:n})}function un(e,t,n,r,i,a){(e.directives||(e.directives=[])).push({name:t,rawName:n,value:r,arg:i,modifiers:a})}function ln(e,t,n,r,i){r&&r.capture&&(delete r.capture,t="!"+t);var a;r&&r.native?(delete r.native,a=e.nativeEvents||(e.nativeEvents={})):a=e.events||(e.events={});var o={value:n,modifiers:r},s=a[t];Array.isArray(s)?i?s.unshift(o):s.push(o):s?a[t]=i?[o,s]:[s,o]:a[t]=o}function fn(e,t,n){var r=dn(e,":"+t)||dn(e,"v-bind:"+t);if(null!=r)return r;if(n!==!1){var i=dn(e,t);if(null!=i)return JSON.stringify(i)}}function dn(e,t){var n;if(null!=(n=e.attrsMap[t]))for(var r=e.attrsList,i=0,a=r.length;i<a;i++)if(r[i].name===t){r.splice(i,1);break}return n}function pn(e,t){Ta=t.warn||an,Sa=t.getTagNamespace||Or,Ea=t.mustUseProp||Or,ja=t.isPreTag||Or,La=on(t.modules,"preTransformNode"),Na=on(t.modules,"transformNode"),Da=on(t.modules,"postTransformNode"),Ma=t.delimiters;var n,r,i=[],a=t.preserveWhitespace!==!1,o=!1,s=!1;return en(e,{expectHTML:t.expectHTML,isUnaryTag:t.isUnaryTag,shouldDecodeNewlines:t.shouldDecodeNewlines,start:function(e,a,c){function u(e){}var l=r&&r.ns||Sa(e);t.isIE&&"svg"===l&&(a=En(a));var f={type:1,tag:e,attrsList:a,attrsMap:On(a,t.isIE),parent:r,children:[]};l&&(f.ns=l),Sn(f)&&(f.forbidden=!0);for(var d=0;d<La.length;d++)La[d](f,t);if(o||(vn(f),f.pre&&(o=!0)),ja(f.tag)&&(s=!0),o)hn(f);else{yn(f),_n(f),$n(f),mn(f),f.plain=!f.key&&!a.length,gn(f),wn(f),Cn(f);for(var p=0;p<Na.length;p++)Na[p](f,t);xn(f)}n||(n=f,u(n)),r&&!f.forbidden&&(f.else?bn(f,r):(r.children.push(f),f.parent=r)),c||(r=f,i.push(f));for(var v=0;v<Da.length;v++)Da[v](f,t)},end:function(){var e=i[i.length-1],t=e.children[e.children.length-1];t&&3===t.type&&" "===t.text&&e.children.pop(),i.length-=1,r=i[i.length-1],e.pre&&(o=!1),ja(e.tag)&&(s=!1)},chars:function(e){if(r&&(e=s||e.trim()?uo(e):a&&r.children.length?" ":"")){var t;!o&&" "!==e&&(t=rn(e,Ma))?r.children.push({type:2,expression:t,text:e}):(e=e.replace(co,""),r.children.push({type:3,text:e}))}}}),n}function vn(e){null!=dn(e,"v-pre")&&(e.pre=!0)}function hn(e){var t=e.attrsList.length;if(t)for(var n=e.attrs=new Array(t),r=0;r<t;r++)n[r]={name:e.attrsList[r].name,value:JSON.stringify(e.attrsList[r].value)};else e.pre||(e.plain=!0)}function mn(e){var t=fn(e,"key");t&&(e.key=t)}function gn(e){var t=fn(e,"ref");t&&(e.ref=t,e.refInFor=kn(e))}function yn(e){var t;if(t=dn(e,"v-for")){var n=t.match(no);if(!n)return;e.for=n[2].trim();var r=n[1].trim(),i=r.match(ro);i?(e.alias=i[1].trim(),e.iterator1=i[2].trim(),i[3]&&(e.iterator2=i[3].trim())):e.alias=r}}function _n(e){var t=dn(e,"v-if");t&&(e.if=t),null!=dn(e,"v-else")&&(e.else=!0)}function bn(e,t){var n=Tn(t.children);n&&n.if&&(n.elseBlock=e)}function $n(e){var t=dn(e,"v-once");null!=t&&(e.once=!0)}function wn(e){if("slot"===e.tag)e.slotName=fn(e,"name");else{var t=fn(e,"slot");t&&(e.slotTarget=t)}}function Cn(e){var t;(t=fn(e,"is"))&&(e.component=t),null!=dn(e,"inline-template")&&(e.inlineTemplate=!0)}function xn(e){var t,n,r,i,a,o,s,c,u=e.attrsList;for(t=0,n=u.length;t<n;t++)if(r=i=u[t].name,a=u[t].value,to.test(r))if(e.hasBindings=!0,s=An(r),s&&(r=r.replace(so,"")),io.test(r))r=r.replace(io,""),s&&s.prop&&(c=!0,r=$r(r),"innerHtml"===r&&(r="innerHTML")),c||Ea(r)?sn(e,r,a):cn(e,r,a);else if(ao.test(r))r=r.replace(ao,""),ln(e,r,a,s);else{r=r.replace(to,"");var l=r.match(oo);l&&(o=l[1])&&(r=r.slice(0,-(o.length+1))),un(e,r,i,a,o,s)}else cn(e,r,JSON.stringify(a))}function kn(e){for(var t=e;t;){if(void 0!==t.for)return!0;t=t.parent}return!1}function An(e){var t=e.match(so);if(t){var n={};return t.forEach(function(e){n[e.slice(1)]=!0}),n}}function On(e,t){for(var n={},r=0,i=e.length;r<i;r++)n[e[r].name]=e[r].value;return n}function Tn(e){for(var t=e.length;t--;)if(e[t].tag)return e[t]}function Sn(e){return"style"===e.tag||"script"===e.tag&&(!e.attrsMap.type||"text/javascript"===e.attrsMap.type)}function En(e){for(var t=[],n=0;n<e.length;n++){var r=e[n];lo.test(r.name)||(r.name=r.name.replace(fo,""),t.push(r))}return t}function jn(e,t){e&&(Pa=po(t.staticKeys||""),Ra=t.isReservedTag||function(){return!1},Nn(e),Dn(e,!1))}function Ln(e){return n("type,tag,attrsList,attrsMap,plain,parent,children,attrs"+(e?","+e:""))}function Nn(e){if(e.static=Mn(e),1===e.type)for(var t=0,n=e.children.length;t<n;t++){var r=e.children[t];Nn(r),r.static||(e.static=!1)}}function Dn(e,t){if(1===e.type){if(e.once||e.static)return e.staticRoot=!0,void(e.staticInFor=t);if(e.children)for(var n=0,r=e.children.length;n<r;n++)Dn(e.children[n],t||!!e.for)}}function Mn(e){return 2!==e.type&&(3===e.type||!(!e.pre&&(e.hasBindings||e.if||e.for||yr(e.tag)||!Ra(e.tag)||Pn(e)||!Object.keys(e).every(Pa))))}function Pn(e){for(;e.parent;){if(e=e.parent,"template"!==e.tag)return!1;if(e.for)return!0}return!1}function Rn(e,t){var n=t?"nativeOn:{":"on:{";for(var r in e)n+='"'+r+'":'+In(e[r])+",";return n.slice(0,-1)+"}"}function In(e){if(e){if(Array.isArray(e))return"["+e.map(In).join(",")+"]";if(e.modifiers){var t="",n=[];for(var r in e.modifiers)mo[r]?t+=mo[r]:n.push(r);n.length&&(t=Bn(n)+t);var i=vo.test(e.value)?e.value+"($event)":e.value;return"function($event){"+t+i+"}"}return vo.test(e.value)?e.value:"function($event){"+e.value+"}"}return"function(){}"}function Bn(e){var t=1===e.length?Fn(e[0]):Array.prototype.concat.apply([],e.map(Fn));return Array.isArray(t)?"if("+t.map(function(e){return"$event.keyCode!=="+e}).join("&&")+")return;":"if($event.keyCode!=="+t+")return;"}function Fn(e){return parseInt(e,10)||ho[e]||"_k("+JSON.stringify(e)+")"}function Hn(e,t){e.wrapData=function(e){return"_b("+e+","+t.value+(t.modifiers&&t.modifiers.prop?",true":"")+")"}}function Un(e,t){var n=Ua,r=Ua=[];za=t,Ia=t.warn||an,Ba=on(t.modules,"transformCode"),Fa=on(t.modules,"genData"),Ha=t.directives||{};var i=e?zn(e):'_h("div")';return Ua=n,{render:"with(this){return "+i+"}",staticRenderFns:r}}function zn(e){if(e.staticRoot&&!e.staticProcessed)return e.staticProcessed=!0,Ua.push("with(this){return "+zn(e)+"}"),"_m("+(Ua.length-1)+(e.staticInFor?",true":"")+")";if(e.for&&!e.forProcessed)return qn(e);if(e.if&&!e.ifProcessed)return Vn(e);if("template"!==e.tag||e.slotTarget){if("slot"===e.tag)return Qn(e);var t;if(e.component)t=Xn(e);else{var n=Kn(e),r=e.inlineTemplate?null:Zn(e);t="_h('"+e.tag+"'"+(n?","+n:"")+(r?","+r:"")+")"}for(var i=0;i<Ba.length;i++)t=Ba[i](e,t);return t}return Zn(e)||"void 0"}function Vn(e){var t=e.if;return e.ifProcessed=!0,"("+t+")?"+zn(e)+":"+Jn(e)}function Jn(e){return e.elseBlock?zn(e.elseBlock):"_e()"}function qn(e){var t=e.for,n=e.alias,r=e.iterator1?","+e.iterator1:"",i=e.iterator2?","+e.iterator2:"";return e.forProcessed=!0,"_l(("+t+"),function("+n+r+i+"){return "+zn(e)+"})"}function Kn(e){if(!e.plain){var t="{",n=Wn(e);n&&(t+=n+","),e.key&&(t+="key:"+e.key+","),e.ref&&(t+="ref:"+e.ref+","),e.refInFor&&(t+="refInFor:true,"),e.component&&(t+='tag:"'+e.tag+'",'),e.slotTarget&&(t+="slot:"+e.slotTarget+",");for(var r=0;r<Fa.length;r++)t+=Fa[r](e);if(e.attrs&&(t+="attrs:{"+er(e.attrs)+"},"),e.props&&(t+="domProps:{"+er(e.props)+"},"),e.events&&(t+=Rn(e.events)+","),e.nativeEvents&&(t+=Rn(e.nativeEvents,!0)+","),e.inlineTemplate){var i=e.children[0];if(1===i.type){var a=Un(i,za);t+="inlineTemplate:{render:function(){"+a.render+"},staticRenderFns:["+a.staticRenderFns.map(function(e){return"function(){"+e+"}"}).join(",")+"]}"}}return t=t.replace(/,$/,"")+"}",e.wrapData&&(t=e.wrapData(t)),t}}function Wn(e){var t=e.directives;if(t){var n,r,i,a,o="directives:[",s=!1;for(n=0,r=t.length;n<r;n++){i=t[n],a=!0;var c=Ha[i.name]||go[i.name];c&&(a=!!c(e,i,Ia)),a&&(s=!0,o+='{name:"'+i.name+'",rawName:"'+i.rawName+'"'+(i.value?",value:("+i.value+"),expression:"+JSON.stringify(i.value):"")+(i.arg?',arg:"'+i.arg+'"':"")+(i.modifiers?",modifiers:"+JSON.stringify(i.modifiers):"")+"},")}return s?o.slice(0,-1)+"]":void 0}}function Zn(e){if(e.children.length)return"["+e.children.map(Gn).join(",")+"]"}function Gn(e){return 1===e.type?zn(e):Yn(e)}function Yn(e){return 2===e.type?e.expression:JSON.stringify(e.text)}function Qn(e){var t=e.slotName||'"default"',n=Zn(e);return n?"_t("+t+","+n+")":"_t("+t+")"}function Xn(e){var t=e.inlineTemplate?null:Zn(e);return"_h("+e.component+","+Kn(e)+(t?","+t:"")+")"}function er(e){for(var t="",n=0;n<e.length;n++){var r=e[n];t+='"'+r.name+'":'+r.value+","}return t.slice(0,-1)}function tr(e,t){var n=pn(e.trim(),t);jn(n,t);var r=Un(n,t);return{ast:n,render:r.render,staticRenderFns:r.staticRenderFns}}function nr(e,t){var n=(t.warn||an,dn(e,"class"));n&&(e.staticClass=JSON.stringify(n));var r=fn(e,"class",!1);r&&(e.classBinding=r)}function rr(e){var t="";return e.staticClass&&(t+="staticClass:"+e.staticClass+","),e.classBinding&&(t+="class:"+e.classBinding+","),t}function ir(e){var t=fn(e,"style",!1);t&&(e.styleBinding=t)}function ar(e){return e.styleBinding?"style:("+e.styleBinding+"),":""}function or(e,t,n){Va=n;var r=t.value,i=t.modifiers,a=e.tag,o=e.attrsMap.type;return"select"===a?lr(e,r):"input"===a&&"checkbox"===o?sr(e,r):"input"===a&&"radio"===o?cr(e,r):ur(e,r,i),!0}function sr(e,t){var n=fn(e,"value")||"null",r=fn(e,"true-value")||"true",i=fn(e,"false-value")||"false";sn(e,"checked","Array.isArray("+t+")?_i("+t+","+n+")>-1:_q("+t+","+r+")"),ln(e,"change","var $$a="+t+",$$el=$event.target,$$c=$$el.checked?("+r+"):("+i+");if(Array.isArray($$a)){var $$v="+n+",$$i=_i($$a,$$v);if($$c){$$i<0&&("+t+"=$$a.concat($$v))}else{$$i>-1&&("+t+"=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}}else{"+t+"=$$c}",null,!0)}function cr(e,t){var n=fn(e,"value")||"null";sn(e,"checked","_q("+t+","+n+")"),ln(e,"change",t+"="+n,null,!0)}function ur(e,t,n){var r=e.attrsMap.type,i=n||{},a=i.lazy,o=i.number,s=i.trim,c=a||Nr&&"range"===r?"change":"input",u=!a&&"range"!==r,l="input"===e.tag||"textarea"===e.tag,f=l?"$event.target.value"+(s?".trim()":""):"$event",d=o||"number"===r?t+"=_n("+f+")":t+"="+f;l&&u&&(d="if($event.target.composing)return;"+d),sn(e,"value",l?"_s("+t+")":"("+t+")"),ln(e,c,d,null,!0)}function lr(e,t){var n=t+'=Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){return "_value" in o ? o._value : o.value})'+(null==e.attrsMap.multiple?"[0]":"");ln(e,"change",n,null,!0)}function fr(e,t){t.value&&sn(e,"textContent","_s("+t.value+")")}function dr(e,t){t.value&&sn(e,"innerHTML","_s("+t.value+")")}function pr(e,t){return t=t?u(u({},Co),t):Co,tr(e,t)}function vr(e,t,n){var r=(t&&t.warn||li,t&&t.delimiters?String(t.delimiters)+e:e);if(wo[r])return wo[r];var i={},a=pr(e,t);i.render=hr(a.render);var o=a.staticRenderFns.length;i.staticRenderFns=new Array(o);for(var s=0;s<o;s++)i.staticRenderFns[s]=hr(a.staticRenderFns[s]);return wo[r]=i}function hr(e){try{return new Function(e)}catch(e){return p}}function mr(e){if(e.outerHTML)return e.outerHTML;var t=document.createElement("div");return t.appendChild(e.cloneNode(!0)),t.innerHTML}var gr,yr=n("slot,component",!0),_r=Object.prototype.hasOwnProperty,br=/-(\w)/g,$r=o(function(e){return e.replace(br,function(e,t){return t?t.toUpperCase():""})}),wr=o(function(e){return e.charAt(0).toUpperCase()+e.slice(1)}),Cr=/([^-])([A-Z])/g,xr=o(function(e){return e.replace(Cr,"$1-$2").replace(Cr,"$1-$2").toLowerCase()}),kr=Object.prototype.toString,Ar="[object Object]",Or=function(){return!1},Tr={optionMergeStrategies:Object.create(null),silent:!1,devtools:!1,errorHandler:null,ignoredElements:null,keyCodes:Object.create(null),isReservedTag:Or,isUnknownElement:Or,getTagNamespace:p,mustUseProp:Or,_assetTypes:["component","directive","filter"],_lifecycleHooks:["beforeCreate","created","beforeMount","mounted","beforeUpdate","updated","beforeDestroy","destroyed","activated","deactivated"],_maxUpdateCount:100,_isServer:!1},Sr=/[^\w\.\$]/,Er="__proto__"in{},jr="undefined"!=typeof window&&"[object Object]"!==Object.prototype.toString.call(window),Lr=jr&&window.navigator.userAgent.toLowerCase(),Nr=Lr&&/msie|trident/.test(Lr),Dr=Lr&&Lr.indexOf("msie 9.0")>0,Mr=Lr&&Lr.indexOf("edge/")>0,Pr=Lr&&Lr.indexOf("android")>0,Rr=Lr&&/iphone|ipad|ipod|ios/.test(Lr),Ir=jr&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__,Br=function(){function e(){r=!1;var e=n.slice(0);n.length=0;for(var t=0;t<e.length;t++)e[t]()}var t,n=[],r=!1;if("undefined"!=typeof Promise&&b(Promise)){var i=Promise.resolve();t=function(){i.then(e),Rr&&setTimeout(p)}}else if("undefined"==typeof MutationObserver||!b(MutationObserver)&&"[object MutationObserverConstructor]"!==MutationObserver.toString())t=function(){setTimeout(e,0)};else{var a=1,o=new MutationObserver(e),s=document.createTextNode(String(a));o.observe(s,{characterData:!0}),t=function(){a=(a+1)%2,s.data=String(a)}}return function(e,i){var a=i?function(){e.call(i)}:e;n.push(a),r||(r=!0,t())}}();gr="undefined"!=typeof Set&&b(Set)?Set:function(){function e(){this.set=Object.create(null)}return e.prototype.has=function(e){return void 0!==this.set[e]},e.prototype.add=function(e){this.set[e]=1},e.prototype.clear=function(){this.set=Object.create(null)},e}();var Fr=0,Hr=function(){this.id=Fr++,this.subs=[]};Hr.prototype.addSub=function(e){this.subs.push(e)},Hr.prototype.removeSub=function(e){r(this.subs,e)},Hr.prototype.depend=function(){Hr.target&&Hr.target.addDep(this)},Hr.prototype.notify=function(){for(var e=this.subs.slice(),t=0,n=e.length;t<n;t++)e[t].update()},Hr.target=null;var Ur=[],zr=[],Vr={},Jr=!1,qr=!1,Kr=0,Wr=0,Zr=function(e,t,n,r){void 0===r&&(r={}),this.vm=e,e._watchers.push(this),this.deep=!!r.deep,this.user=!!r.user,this.lazy=!!r.lazy,this.sync=!!r.sync,this.expression=t.toString(),this.cb=n,this.id=++Wr,this.active=!0,this.dirty=this.lazy,this.deps=[],this.newDeps=[],this.depIds=new gr,this.newDepIds=new gr,"function"==typeof t?this.getter=t:(this.getter=_(t),this.getter||(this.getter=function(){})),this.value=this.lazy?void 0:this.get()};Zr.prototype.get=function(){$(this);var e=this.getter.call(this.vm,this.vm);return this.deep&&A(e),w(),this.cleanupDeps(),e},Zr.prototype.addDep=function(e){var t=e.id;this.newDepIds.has(t)||(this.newDepIds.add(t),this.newDeps.push(e),this.depIds.has(t)||e.addSub(this))},Zr.prototype.cleanupDeps=function(){for(var e=this,t=this.deps.length;t--;){var n=e.deps[t];e.newDepIds.has(n.id)||n.removeSub(e)}var r=this.depIds;this.depIds=this.newDepIds,this.newDepIds=r,this.newDepIds.clear(),r=this.deps,this.deps=this.newDeps,this.newDeps=r,this.newDeps.length=0},Zr.prototype.update=function(){this.lazy?this.dirty=!0:this.sync?this.run():k(this)},Zr.prototype.run=function(){if(this.active){var e=this.get();if(e!==this.value||l(e)||this.deep){var t=this.value;if(this.value=e,this.user)try{this.cb.call(this.vm,e,t)}catch(e){if(!Tr.errorHandler)throw e;Tr.errorHandler.call(null,e,this.vm)}else this.cb.call(this.vm,e,t)}}},Zr.prototype.evaluate=function(){this.value=this.get(),this.dirty=!1},Zr.prototype.depend=function(){for(var e=this,t=this.deps.length;t--;)e.deps[t].depend()},Zr.prototype.teardown=function(){var e=this;if(this.active){this.vm._isBeingDestroyed||this.vm._vForRemoving||r(this.vm._watchers,this);for(var t=this.deps.length;t--;)e.deps[t].removeSub(e);this.active=!1}};var Gr=new gr,Yr=Array.prototype,Qr=Object.create(Yr);["push","pop","shift","unshift","splice","sort","reverse"].forEach(function(e){var t=Yr[e];y(Qr,e,function(){for(var n=arguments,r=arguments.length,i=new Array(r);r--;)i[r]=n[r];var a,o=t.apply(this,i),s=this.__ob__;switch(e){case"push":a=i;break;case"unshift":a=i;break;case"splice":a=i.slice(2)}return a&&s.observeArray(a),s.dep.notify(),o})});var Xr=Object.getOwnPropertyNames(Qr),ei={shouldConvert:!0,isSettingProps:!1},ti=function(e){if(this.value=e,this.dep=new Hr,this.vmCount=0,y(e,"__ob__",this),Array.isArray(e)){var t=Er?O:T;t(e,Qr,Xr),this.observeArray(e)}else this.walk(e)};ti.prototype.walk=function(e){for(var t=Object.keys(e),n=0;n<t.length;n++)E(e,t[n],e[t[n]])},ti.prototype.observeArray=function(e){for(var t=0,n=e.length;t<n;t++)S(e[t])};var ni={enumerable:!0,configurable:!0,get:p,set:p},ri=function(e,t,n,r,i,a,o,s){this.tag=e,this.data=t,this.children=n,this.text=r,this.elm=i,this.ns=a,this.context=o,this.functionalContext=void 0,this.key=t&&t.key,this.componentOptions=s,this.child=void 0,this.parent=void 0,this.raw=!1,this.isStatic=!1,this.isRootInsert=!0,this.isComment=!1,this.isCloned=!1},ii=function(){var e=new ri;return e.text="",e.isComment=!0,e},ai=null,oi={init:oe,prepatch:se,insert:ce,destroy:ue},si=Object.keys(oi),ci=0;we(Ce),U(Ce),$e(Ce),te(Ce),ye(Ce);var ui,li=p,fi=Tr.optionMergeStrategies;fi.data=function(e,t,n){return n?e||t?function(){var r="function"==typeof t?t.call(n):t,i="function"==typeof e?e.call(n):void 0;return r?xe(r,i):i}:void 0:t?"function"!=typeof t?e:e?function(){return xe(t.call(this),e.call(this))}:t:e},Tr._lifecycleHooks.forEach(function(e){fi[e]=ke}),Tr._assetTypes.forEach(function(e){fi[e+"s"]=Ae}),fi.watch=function(e,t){if(!t)return e;if(!e)return t;var n={};u(n,e);for(var r in t){var i=n[r],a=t[r];i&&!Array.isArray(i)&&(i=[i]),n[r]=i?i.concat(a):[a]}return n},fi.props=fi.methods=fi.computed=function(e,t){if(!t)return e;if(!e)return t;var n=Object.create(null);return u(n,e),u(n,t),n};var di=function(e,t){return void 0===t?e:t},pi=Object.freeze({defineReactive:E,_toString:e,toNumber:t,makeMap:n,isBuiltInTag:yr,remove:r,hasOwn:i,isPrimitive:a,cached:o,camelize:$r,capitalize:wr,hyphenate:xr,bind:s,toArray:c,extend:u,isObject:l,isPlainObject:f,toObject:d,noop:p,no:Or,genStaticKeys:v,looseEqual:h,looseIndexOf:m,isReserved:g,def:y,parsePath:_,hasProto:Er,inBrowser:jr,UA:Lr,isIE:Nr,isIE9:Dr,isEdge:Mr,isAndroid:Pr,isIOS:Rr,devtools:Ir,nextTick:Br,get _Set(){return gr},mergeOptions:Ee,resolveAsset:je,warn:li,formatComponentName:ui,validateProp:Le}),vi={name:"keep-alive",abstract:!0,created:function(){this.cache=Object.create(null)},render:function(){var e=X(this.$slots.default);if(e&&e.componentOptions){var t=e.componentOptions,n=null==e.key?t.Ctor.cid+"::"+t.tag:e.key;this.cache[n]?e.child=this.cache[n].child:this.cache[n]=e,e.data.keepAlive=!0}return e},destroyed:function(){var e=this;for(var t in this.cache){var n=e.cache[t];ne(n.child,"deactivated"),n.child.$destroy()}}},hi={KeepAlive:vi};Fe(Ce),Object.defineProperty(Ce.prototype,"$isServer",{get:function(){return Tr._isServer}}),Ce.version="2.0.3";var mi,gi=n("value,selected,checked,muted"),yi=n("contenteditable,draggable,spellcheck"),_i=n("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,translate,truespeed,typemustmatch,visible"),bi="http://www.w3.org/1999/xlink",$i=function(e){return":"===e.charAt(5)&&"xlink"===e.slice(0,5)},wi=function(e){return $i(e)?e.slice(6,e.length):""},Ci=function(e){return null==e||e===!1},xi={svg:"http://www.w3.org/2000/svg",math:"http://www.w3.org/1998/Math/MathML"},ki=n("html,body,base,head,link,meta,style,title,address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,div,dd,dl,dt,figcaption,figure,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,menuitem,summary,content,element,shadow,template"),Ai=n("area,base,br,col,embed,frame,hr,img,input,isindex,keygen,link,meta,param,source,track,wbr",!0),Oi=n("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source",!0),Ti=n("address,article,aside,base,blockquote,body,caption,col,colgroup,dd,details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,title,tr,track",!0),Si=n("svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font,font-face,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view",!0),Ei=function(e){return"pre"===e},ji=function(e){return ki(e)||Si(e)},Li=Object.create(null),Ni=Object.freeze({createElement:Ze,createElementNS:Ge,createTextNode:Ye,createComment:Qe,insertBefore:Xe,removeChild:et,appendChild:tt,parentNode:nt,nextSibling:rt,tagName:it,setTextContent:at,childNodes:ot,setAttribute:st}),Di={create:function(e,t){ct(t)},update:function(e,t){e.data.ref!==t.data.ref&&(ct(e,!0),ct(t))},destroy:function(e){ct(e,!0)}},Mi=new ri("",{},[]),Pi=["create","update","remove","destroy"],Ri={create:vt,update:vt,destroy:function(e){vt(e,Mi)}},Ii=Object.create(null),Bi=[Di,Ri],Fi={create:yt,update:yt},Hi={create:bt,update:bt},Ui={create:$t,update:$t},zi={create:wt,update:wt},Vi=["Webkit","Moz","ms"],Ji=o(function(e){if(mi=mi||document.createElement("div"),e=$r(e),"filter"!==e&&e in mi.style)return e;for(var t=e.charAt(0).toUpperCase()+e.slice(1),n=0;n<Vi.length;n++){var r=Vi[n]+t;if(r in mi.style)return r}}),qi={create:Ct,update:Ct},Ki=jr&&!Dr,Wi="transition",Zi="animation",Gi="transition",Yi="transitionend",Qi="animation",Xi="animationend";Ki&&(void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend&&(Gi="WebkitTransition",Yi="webkitTransitionEnd"),void 0===window.onanimationend&&void 0!==window.onwebkitanimationend&&(Qi="WebkitAnimation",Xi="webkitAnimationEnd"));var ea=jr&&window.requestAnimationFrame||setTimeout,ta=/\b(transform|all)(,|$)/,na=o(function(e){return{enterClass:e+"-enter",leaveClass:e+"-leave",appearClass:e+"-enter",enterActiveClass:e+"-enter-active",leaveActiveClass:e+"-leave-active",appearActiveClass:e+"-enter-active"}}),ra=jr?{create:function(e,t){t.data.show||Nt(t)},remove:function(e,t){e.data.show?t():Dt(e,t)}}:{},ia=[Fi,Hi,Ui,zi,qi,ra],aa=ia.concat(Bi),oa=pt({nodeOps:Ni,modules:aa});Dr&&document.addEventListener("selectionchange",function(){var e=document.activeElement;e&&e.vmodel&&Ut(e,"input")});var sa={inserted:function(e,t,n){if("select"===n.tag){var r=function(){Rt(e,t,n.context)};r(),(Nr||Mr)&&setTimeout(r,0)}else"textarea"!==n.tag&&"text"!==e.type||t.modifiers.lazy||(Pr||(e.addEventListener("compositionstart",Ft),e.addEventListener("compositionend",Ht)),Dr&&(e.vmodel=!0))},componentUpdated:function(e,t,n){if("select"===n.tag){Rt(e,t,n.context);var r=e.multiple?t.value.some(function(t){return It(t,e.options)}):t.value!==t.oldValue&&It(t.value,e.options);r&&Ut(e,"change")}}},ca={bind:function(e,t,n){var r=t.value;n=zt(n);var i=n.data&&n.data.transition;r&&i&&!Dr&&Nt(n);var a="none"===e.style.display?"":e.style.display;e.style.display=r?a:"none",e.__vOriginalDisplay=a},update:function(e,t,n){var r=t.value,i=t.oldValue;if(r!==i){n=zt(n);var a=n.data&&n.data.transition;a&&!Dr?r?(Nt(n),e.style.display=e.__vOriginalDisplay):Dt(n,function(){e.style.display="none"}):e.style.display=r?e.__vOriginalDisplay:"none"}}},ua={model:sa,show:ca},la={name:String,appear:Boolean,css:Boolean,mode:String,type:String,enterClass:String,leaveClass:String,enterActiveClass:String,leaveActiveClass:String,appearClass:String,appearActiveClass:String},fa={name:"transition",props:la,abstract:!0,render:function(e){var t=this,n=this.$slots.default;if(n&&(n=n.filter(function(e){return e.tag}),n.length)){var r=this.mode,i=n[0];if(Kt(this.$vnode))return i;var a=Vt(i);if(!a)return i;if(this._leaving)return qt(e,i);var o=a.key=null==a.key||a.isStatic?"__v"+(a.tag+this._uid)+"__":a.key,s=(a.data||(a.data={})).transition=Jt(this),c=this._vnode,l=Vt(c);if(a.data.directives&&a.data.directives.some(function(e){return"show"===e.name})&&(a.data.show=!0),l&&l.data&&l.key!==o){var f=l.data.transition=u({},s);if("out-in"===r)return this._leaving=!0,q(f,"afterLeave",function(){t._leaving=!1,t.$forceUpdate()},o),qt(e,i);if("in-out"===r){var d,p=function(){d()};q(s,"afterEnter",p,o),q(s,"enterCancelled",p,o),q(f,"delayLeave",function(e){d=e},o)}}return i}}},da=u({tag:String,moveClass:String},la);delete da.mode;var pa={props:da,render:function(e){for(var t=this.tag||this.$vnode.data.tag||"span",n=Object.create(null),r=this.prevChildren=this.children,i=this.$slots.default||[],a=this.children=[],o=Jt(this),s=0;s<i.length;s++){var c=i[s];c.tag&&null!=c.key&&0!==String(c.key).indexOf("__vlist")&&(a.push(c),n[c.key]=c,(c.data||(c.data={})).transition=o)}if(r){for(var u=[],l=[],f=0;f<r.length;f++){var d=r[f];d.data.transition=o,d.data.pos=d.elm.getBoundingClientRect(),n[d.key]?u.push(d):l.push(d)}this.kept=e(t,null,u),this.removed=l}return e(t,null,a)},beforeUpdate:function(){this.__patch__(this._vnode,this.kept,!1,!0),this._vnode=this.kept},updated:function(){var e=this.prevChildren,t=this.moveClass||this.name+"-move";if(e.length&&this.hasMove(e[0].elm,t)){e.forEach(Wt),e.forEach(Zt),e.forEach(Gt);document.body.offsetHeight;e.forEach(function(e){if(e.data.moved){var n=e.elm,r=n.style;Ot(n,t),r.transform=r.WebkitTransform=r.transitionDuration="",n.addEventListener(Yi,n._moveCb=function e(r){r&&!/transform$/.test(r.propertyName)||(n.removeEventListener(Yi,e),n._moveCb=null,Tt(n,t))})}})}},methods:{hasMove:function(e,t){if(!Ki)return!1;if(null!=this._hasMove)return this._hasMove;Ot(e,t);var n=Et(e);return Tt(e,t),this._hasMove=n.hasTransform}}},va={Transition:fa,TransitionGroup:pa};Ce.config.isUnknownElement=Ke,Ce.config.isReservedTag=ji,Ce.config.getTagNamespace=qe,Ce.config.mustUseProp=gi,u(Ce.options.directives,ua),u(Ce.options.components,va),Ce.prototype.__patch__=Tr._isServer?p:oa,Ce.prototype.$mount=function(e,t){return e=e&&!Tr._isServer?We(e):void 0,this._mount(e,t)},setTimeout(function(){Tr.devtools&&Ir&&Ir.emit("init",Ce)},0);var ha=!!jr&&Yt("\n","&#10;"),ma=document.createElement("div"),ga=/([^\s"'<>\/=]+)/,ya=/(?:=)/,_a=[/"([^"]*)"+/.source,/'([^']*)'+/.source,/([^\s"'=<>`]+)/.source],ba=new RegExp("^\\s*"+ga.source+"(?:\\s*("+ya.source+")\\s*(?:"+_a.join("|")+"))?"),$a="[a-zA-Z_][\\w\\-\\.]*",wa="((?:"+$a+"\\:)?"+$a+")",Ca=new RegExp("^<"+wa),xa=/^\s*(\/?)>/,ka=new RegExp("^<\\/"+wa+"[^>]*>"),Aa=/^<!DOCTYPE [^>]+>/i,Oa=!1;"x".replace(/x(.)?/g,function(e,t){Oa=""===t});var Ta,Sa,Ea,ja,La,Na,Da,Ma,Pa,Ra,Ia,Ba,Fa,Ha,Ua,za,Va,Ja=n("script,style",!0),qa={},Ka=/&lt;/g,Wa=/&gt;/g,Za=/&#10;/g,Ga=/&amp;/g,Ya=/&quot;/g,Qa=/\{\{((?:.|\n)+?)\}\}/g,Xa=/[-.*+?^${}()|[\]\/\\]/g,eo=o(function(e){var t=e[0].replace(Xa,"\\$&"),n=e[1].replace(Xa,"\\$&");return new RegExp(t+"((?:.|\\n)+?)"+n,"g")}),to=/^v-|^@|^:/,no=/(.*?)\s+(?:in|of)\s+(.*)/,ro=/\(([^,]*),([^,]*)(?:,([^,]*))?\)/,io=/^:|^v-bind:/,ao=/^@|^v-on:/,oo=/:(.*)$/,so=/\.[^\.]+/g,co=/\u2028|\u2029/g,uo=o(Qt),lo=/^xmlns:NS\d+/,fo=/^NS\d+:/,po=o(Ln),vo=/^\s*[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*\s*$/,ho={esc:27,tab:9,enter:13,space:32,up:38,left:37,right:39,down:40,delete:[8,46]},mo={stop:"$event.stopPropagation();",prevent:"$event.preventDefault();",self:"if($event.target !== $event.currentTarget)return;"},go={bind:Hn,cloak:p},yo=(new RegExp("\\b"+"do,if,for,let,new,try,var,case,else,with,await,break,catch,class,const,super,throw,while,yield,delete,export,import,return,switch,default,extends,finally,continue,debugger,function,arguments".split(",").join("\\b|\\b")+"\\b"),{staticKeys:["staticClass"],transformNode:nr,genData:rr}),_o={transformNode:ir,genData:ar},bo=[yo,_o],$o={model:or,text:fr,html:dr},wo=Object.create(null),Co={isIE:Nr,expectHTML:!0,modules:bo,staticKeys:v(bo),directives:$o,isReservedTag:ji,isUnaryTag:Ai,mustUseProp:gi,getTagNamespace:qe,isPreTag:Ei},xo=o(function(e){var t=We(e);return t&&t.innerHTML}),ko=Ce.prototype.$mount;return Ce.prototype.$mount=function(e,t){if(e=e&&We(e),e===document.body||e===document.documentElement)return this;var n=this.$options;if(!n.render){var r=n.template;if(r)if("string"==typeof r)"#"===r.charAt(0)&&(r=xo(r));else{if(!r.nodeType)return this;r=r.innerHTML}else e&&(r=mr(e));if(r){var i=vr(r,{warn:li,shouldDecodeNewlines:ha,delimiters:n.delimiters},this),a=i.render,o=i.staticRenderFns;n.render=a,n.staticRenderFns=o}}return ko.call(this,e,t)},Ce.compile=vr,Ce}); \ No newline at end of file
diff --git a/vendor/assets/stylesheets/katex.css b/vendor/assets/stylesheets/katex.css
new file mode 100644
index 00000000000..3e62df2329c
--- /dev/null
+++ b/vendor/assets/stylesheets/katex.css
@@ -0,0 +1,975 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2015 Khan Academy
+
+This software also uses portions of the underscore.js project, which is
+MIT licensed with the following copyright:
+
+Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative
+Reporters & Editors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+/*
+ Here is how to build a version of KaTeX that works with gitlab.
+
+ The problem is that the standard procedure for changing font location doesn't work for the empty string.
+
+ 1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do.
+ 2. make (requires node)
+ 3. sed -i 's,fonts/,,' build/katex.css
+ 4. Copy build/katex.js, build/katex.css and fonts/* to gitlab.
+*/
+
+@font-face {
+ font-family: 'KaTeX_AMS';
+ src: url('KaTeX_AMS-Regular.eot');
+ src: url('KaTeX_AMS-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_AMS-Regular.woff2') format('woff2'), url('KaTeX_AMS-Regular.woff') format('woff'), url('KaTeX_AMS-Regular.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Caligraphic';
+ src: url('KaTeX_Caligraphic-Bold.eot');
+ src: url('KaTeX_Caligraphic-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Caligraphic-Bold.woff2') format('woff2'), url('KaTeX_Caligraphic-Bold.woff') format('woff'), url('KaTeX_Caligraphic-Bold.ttf') format('truetype');
+ font-weight: bold;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Caligraphic';
+ src: url('KaTeX_Caligraphic-Regular.eot');
+ src: url('KaTeX_Caligraphic-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Caligraphic-Regular.woff2') format('woff2'), url('KaTeX_Caligraphic-Regular.woff') format('woff'), url('KaTeX_Caligraphic-Regular.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Fraktur';
+ src: url('KaTeX_Fraktur-Bold.eot');
+ src: url('KaTeX_Fraktur-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Fraktur-Bold.woff2') format('woff2'), url('KaTeX_Fraktur-Bold.woff') format('woff'), url('KaTeX_Fraktur-Bold.ttf') format('truetype');
+ font-weight: bold;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Fraktur';
+ src: url('KaTeX_Fraktur-Regular.eot');
+ src: url('KaTeX_Fraktur-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Fraktur-Regular.woff2') format('woff2'), url('KaTeX_Fraktur-Regular.woff') format('woff'), url('KaTeX_Fraktur-Regular.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Main';
+ src: url('KaTeX_Main-Bold.eot');
+ src: url('KaTeX_Main-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Bold.woff2') format('woff2'), url('KaTeX_Main-Bold.woff') format('woff'), url('KaTeX_Main-Bold.ttf') format('truetype');
+ font-weight: bold;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Main';
+ src: url('KaTeX_Main-Italic.eot');
+ src: url('KaTeX_Main-Italic.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Italic.woff2') format('woff2'), url('KaTeX_Main-Italic.woff') format('woff'), url('KaTeX_Main-Italic.ttf') format('truetype');
+ font-weight: normal;
+ font-style: italic;
+}
+@font-face {
+ font-family: 'KaTeX_Main';
+ src: url('KaTeX_Main-Regular.eot');
+ src: url('KaTeX_Main-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Regular.woff2') format('woff2'), url('KaTeX_Main-Regular.woff') format('woff'), url('KaTeX_Main-Regular.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Math';
+ src: url('KaTeX_Math-Italic.eot');
+ src: url('KaTeX_Math-Italic.eot#iefix') format('embedded-opentype'), url('KaTeX_Math-Italic.woff2') format('woff2'), url('KaTeX_Math-Italic.woff') format('woff'), url('KaTeX_Math-Italic.ttf') format('truetype');
+ font-weight: normal;
+ font-style: italic;
+}
+@font-face {
+ font-family: 'KaTeX_SansSerif';
+ src: url('KaTeX_SansSerif-Regular.eot');
+ src: url('KaTeX_SansSerif-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_SansSerif-Regular.woff2') format('woff2'), url('KaTeX_SansSerif-Regular.woff') format('woff'), url('KaTeX_SansSerif-Regular.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Script';
+ src: url('KaTeX_Script-Regular.eot');
+ src: url('KaTeX_Script-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Script-Regular.woff2') format('woff2'), url('KaTeX_Script-Regular.woff') format('woff'), url('KaTeX_Script-Regular.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Size1';
+ src: url('KaTeX_Size1-Regular.eot');
+ src: url('KaTeX_Size1-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size1-Regular.woff2') format('woff2'), url('KaTeX_Size1-Regular.woff') format('woff'), url('KaTeX_Size1-Regular.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Size2';
+ src: url('KaTeX_Size2-Regular.eot');
+ src: url('KaTeX_Size2-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size2-Regular.woff2') format('woff2'), url('KaTeX_Size2-Regular.woff') format('woff'), url('KaTeX_Size2-Regular.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Size3';
+ src: url('KaTeX_Size3-Regular.eot');
+ src: url('KaTeX_Size3-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size3-Regular.woff2') format('woff2'), url('KaTeX_Size3-Regular.woff') format('woff'), url('KaTeX_Size3-Regular.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Size4';
+ src: url('KaTeX_Size4-Regular.eot');
+ src: url('KaTeX_Size4-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size4-Regular.woff2') format('woff2'), url('KaTeX_Size4-Regular.woff') format('woff'), url('KaTeX_Size4-Regular.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Typewriter';
+ src: url('KaTeX_Typewriter-Regular.eot');
+ src: url('KaTeX_Typewriter-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Typewriter-Regular.woff2') format('woff2'), url('KaTeX_Typewriter-Regular.woff') format('woff'), url('KaTeX_Typewriter-Regular.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+.katex-display {
+ display: block;
+ margin: 1em 0;
+ text-align: center;
+}
+.katex-display > .katex {
+ display: inline-block;
+ text-align: initial;
+}
+.katex {
+ font: normal 1.21em KaTeX_Main, Times New Roman, serif;
+ line-height: 1.2;
+ white-space: nowrap;
+ text-indent: 0;
+}
+.katex .katex-html {
+ display: inline-block;
+}
+.katex .katex-mathml {
+ position: absolute;
+ clip: rect(1px, 1px, 1px, 1px);
+ padding: 0;
+ border: 0;
+ height: 1px;
+ width: 1px;
+ overflow: hidden;
+}
+.katex .base {
+ display: inline-block;
+}
+.katex .strut {
+ display: inline-block;
+}
+.katex .mathit {
+ font-family: KaTeX_Math;
+ font-style: italic;
+}
+.katex .mathbf {
+ font-family: KaTeX_Main;
+ font-weight: bold;
+}
+.katex .amsrm {
+ font-family: KaTeX_AMS;
+}
+.katex .mathbb {
+ font-family: KaTeX_AMS;
+}
+.katex .mathcal {
+ font-family: KaTeX_Caligraphic;
+}
+.katex .mathfrak {
+ font-family: KaTeX_Fraktur;
+}
+.katex .mathtt {
+ font-family: KaTeX_Typewriter;
+}
+.katex .mathscr {
+ font-family: KaTeX_Script;
+}
+.katex .mathsf {
+ font-family: KaTeX_SansSerif;
+}
+.katex .mainit {
+ font-family: KaTeX_Main;
+ font-style: italic;
+}
+.katex .textstyle > .mord + .mop {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mord + .mbin {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .mord + .mrel {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mord + .minner {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mop + .mord {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mop + .mop {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mop + .mrel {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mop + .minner {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mbin + .mord {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .mbin + .mop {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .mbin + .mopen {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .mbin + .minner {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .mrel + .mord {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mrel + .mop {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mrel + .mopen {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mrel + .minner {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mclose + .mop {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mclose + .mbin {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .mclose + .mrel {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mclose + .minner {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mord {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mop {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mrel {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mopen {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mclose {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mpunct {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .minner {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .minner + .mord {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .minner + .mop {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .minner + .mbin {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .minner + .mrel {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .minner + .mopen {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .minner + .mpunct {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .minner + .minner {
+ margin-left: 0.16667em;
+}
+.katex .mord + .mop {
+ margin-left: 0.16667em;
+}
+.katex .mop + .mord {
+ margin-left: 0.16667em;
+}
+.katex .mop + .mop {
+ margin-left: 0.16667em;
+}
+.katex .mclose + .mop {
+ margin-left: 0.16667em;
+}
+.katex .minner + .mop {
+ margin-left: 0.16667em;
+}
+.katex .reset-textstyle.textstyle {
+ font-size: 1em;
+}
+.katex .reset-textstyle.scriptstyle {
+ font-size: 0.7em;
+}
+.katex .reset-textstyle.scriptscriptstyle {
+ font-size: 0.5em;
+}
+.katex .reset-scriptstyle.textstyle {
+ font-size: 1.42857em;
+}
+.katex .reset-scriptstyle.scriptstyle {
+ font-size: 1em;
+}
+.katex .reset-scriptstyle.scriptscriptstyle {
+ font-size: 0.71429em;
+}
+.katex .reset-scriptscriptstyle.textstyle {
+ font-size: 2em;
+}
+.katex .reset-scriptscriptstyle.scriptstyle {
+ font-size: 1.4em;
+}
+.katex .reset-scriptscriptstyle.scriptscriptstyle {
+ font-size: 1em;
+}
+.katex .style-wrap {
+ position: relative;
+}
+.katex .vlist {
+ display: inline-block;
+}
+.katex .vlist > span {
+ display: block;
+ height: 0;
+ position: relative;
+}
+.katex .vlist > span > span {
+ display: inline-block;
+}
+.katex .vlist .baseline-fix {
+ display: inline-table;
+ table-layout: fixed;
+}
+.katex .msupsub {
+ text-align: left;
+}
+.katex .mfrac > span > span {
+ text-align: center;
+}
+.katex .mfrac .frac-line {
+ width: 100%;
+}
+.katex .mfrac .frac-line:before {
+ border-bottom-style: solid;
+ border-bottom-width: 1px;
+ content: "";
+ display: block;
+}
+.katex .mfrac .frac-line:after {
+ border-bottom-style: solid;
+ border-bottom-width: 0.04em;
+ content: "";
+ display: block;
+ margin-top: -1px;
+}
+.katex .mspace {
+ display: inline-block;
+}
+.katex .mspace.negativethinspace {
+ margin-left: -0.16667em;
+}
+.katex .mspace.thinspace {
+ width: 0.16667em;
+}
+.katex .mspace.mediumspace {
+ width: 0.22222em;
+}
+.katex .mspace.thickspace {
+ width: 0.27778em;
+}
+.katex .mspace.enspace {
+ width: 0.5em;
+}
+.katex .mspace.quad {
+ width: 1em;
+}
+.katex .mspace.qquad {
+ width: 2em;
+}
+.katex .llap,
+.katex .rlap {
+ width: 0;
+ position: relative;
+}
+.katex .llap > .inner,
+.katex .rlap > .inner {
+ position: absolute;
+}
+.katex .llap > .fix,
+.katex .rlap > .fix {
+ display: inline-block;
+}
+.katex .llap > .inner {
+ right: 0;
+}
+.katex .rlap > .inner {
+ left: 0;
+}
+.katex .katex-logo .a {
+ font-size: 0.75em;
+ margin-left: -0.32em;
+ position: relative;
+ top: -0.2em;
+}
+.katex .katex-logo .t {
+ margin-left: -0.23em;
+}
+.katex .katex-logo .e {
+ margin-left: -0.1667em;
+ position: relative;
+ top: 0.2155em;
+}
+.katex .katex-logo .x {
+ margin-left: -0.125em;
+}
+.katex .rule {
+ display: inline-block;
+ border: solid 0;
+ position: relative;
+}
+.katex .overline .overline-line,
+.katex .underline .underline-line {
+ width: 100%;
+}
+.katex .overline .overline-line:before,
+.katex .underline .underline-line:before {
+ border-bottom-style: solid;
+ border-bottom-width: 1px;
+ content: "";
+ display: block;
+}
+.katex .overline .overline-line:after,
+.katex .underline .underline-line:after {
+ border-bottom-style: solid;
+ border-bottom-width: 0.04em;
+ content: "";
+ display: block;
+ margin-top: -1px;
+}
+.katex .sqrt > .sqrt-sign {
+ position: relative;
+}
+.katex .sqrt .sqrt-line {
+ width: 100%;
+}
+.katex .sqrt .sqrt-line:before {
+ border-bottom-style: solid;
+ border-bottom-width: 1px;
+ content: "";
+ display: block;
+}
+.katex .sqrt .sqrt-line:after {
+ border-bottom-style: solid;
+ border-bottom-width: 0.04em;
+ content: "";
+ display: block;
+ margin-top: -1px;
+}
+.katex .sqrt > .root {
+ margin-left: 0.27777778em;
+ margin-right: -0.55555556em;
+}
+.katex .sizing,
+.katex .fontsize-ensurer {
+ display: inline-block;
+}
+.katex .sizing.reset-size1.size1,
+.katex .fontsize-ensurer.reset-size1.size1 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size1.size2,
+.katex .fontsize-ensurer.reset-size1.size2 {
+ font-size: 1.4em;
+}
+.katex .sizing.reset-size1.size3,
+.katex .fontsize-ensurer.reset-size1.size3 {
+ font-size: 1.6em;
+}
+.katex .sizing.reset-size1.size4,
+.katex .fontsize-ensurer.reset-size1.size4 {
+ font-size: 1.8em;
+}
+.katex .sizing.reset-size1.size5,
+.katex .fontsize-ensurer.reset-size1.size5 {
+ font-size: 2em;
+}
+.katex .sizing.reset-size1.size6,
+.katex .fontsize-ensurer.reset-size1.size6 {
+ font-size: 2.4em;
+}
+.katex .sizing.reset-size1.size7,
+.katex .fontsize-ensurer.reset-size1.size7 {
+ font-size: 2.88em;
+}
+.katex .sizing.reset-size1.size8,
+.katex .fontsize-ensurer.reset-size1.size8 {
+ font-size: 3.46em;
+}
+.katex .sizing.reset-size1.size9,
+.katex .fontsize-ensurer.reset-size1.size9 {
+ font-size: 4.14em;
+}
+.katex .sizing.reset-size1.size10,
+.katex .fontsize-ensurer.reset-size1.size10 {
+ font-size: 4.98em;
+}
+.katex .sizing.reset-size2.size1,
+.katex .fontsize-ensurer.reset-size2.size1 {
+ font-size: 0.71428571em;
+}
+.katex .sizing.reset-size2.size2,
+.katex .fontsize-ensurer.reset-size2.size2 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size2.size3,
+.katex .fontsize-ensurer.reset-size2.size3 {
+ font-size: 1.14285714em;
+}
+.katex .sizing.reset-size2.size4,
+.katex .fontsize-ensurer.reset-size2.size4 {
+ font-size: 1.28571429em;
+}
+.katex .sizing.reset-size2.size5,
+.katex .fontsize-ensurer.reset-size2.size5 {
+ font-size: 1.42857143em;
+}
+.katex .sizing.reset-size2.size6,
+.katex .fontsize-ensurer.reset-size2.size6 {
+ font-size: 1.71428571em;
+}
+.katex .sizing.reset-size2.size7,
+.katex .fontsize-ensurer.reset-size2.size7 {
+ font-size: 2.05714286em;
+}
+.katex .sizing.reset-size2.size8,
+.katex .fontsize-ensurer.reset-size2.size8 {
+ font-size: 2.47142857em;
+}
+.katex .sizing.reset-size2.size9,
+.katex .fontsize-ensurer.reset-size2.size9 {
+ font-size: 2.95714286em;
+}
+.katex .sizing.reset-size2.size10,
+.katex .fontsize-ensurer.reset-size2.size10 {
+ font-size: 3.55714286em;
+}
+.katex .sizing.reset-size3.size1,
+.katex .fontsize-ensurer.reset-size3.size1 {
+ font-size: 0.625em;
+}
+.katex .sizing.reset-size3.size2,
+.katex .fontsize-ensurer.reset-size3.size2 {
+ font-size: 0.875em;
+}
+.katex .sizing.reset-size3.size3,
+.katex .fontsize-ensurer.reset-size3.size3 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size3.size4,
+.katex .fontsize-ensurer.reset-size3.size4 {
+ font-size: 1.125em;
+}
+.katex .sizing.reset-size3.size5,
+.katex .fontsize-ensurer.reset-size3.size5 {
+ font-size: 1.25em;
+}
+.katex .sizing.reset-size3.size6,
+.katex .fontsize-ensurer.reset-size3.size6 {
+ font-size: 1.5em;
+}
+.katex .sizing.reset-size3.size7,
+.katex .fontsize-ensurer.reset-size3.size7 {
+ font-size: 1.8em;
+}
+.katex .sizing.reset-size3.size8,
+.katex .fontsize-ensurer.reset-size3.size8 {
+ font-size: 2.1625em;
+}
+.katex .sizing.reset-size3.size9,
+.katex .fontsize-ensurer.reset-size3.size9 {
+ font-size: 2.5875em;
+}
+.katex .sizing.reset-size3.size10,
+.katex .fontsize-ensurer.reset-size3.size10 {
+ font-size: 3.1125em;
+}
+.katex .sizing.reset-size4.size1,
+.katex .fontsize-ensurer.reset-size4.size1 {
+ font-size: 0.55555556em;
+}
+.katex .sizing.reset-size4.size2,
+.katex .fontsize-ensurer.reset-size4.size2 {
+ font-size: 0.77777778em;
+}
+.katex .sizing.reset-size4.size3,
+.katex .fontsize-ensurer.reset-size4.size3 {
+ font-size: 0.88888889em;
+}
+.katex .sizing.reset-size4.size4,
+.katex .fontsize-ensurer.reset-size4.size4 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size4.size5,
+.katex .fontsize-ensurer.reset-size4.size5 {
+ font-size: 1.11111111em;
+}
+.katex .sizing.reset-size4.size6,
+.katex .fontsize-ensurer.reset-size4.size6 {
+ font-size: 1.33333333em;
+}
+.katex .sizing.reset-size4.size7,
+.katex .fontsize-ensurer.reset-size4.size7 {
+ font-size: 1.6em;
+}
+.katex .sizing.reset-size4.size8,
+.katex .fontsize-ensurer.reset-size4.size8 {
+ font-size: 1.92222222em;
+}
+.katex .sizing.reset-size4.size9,
+.katex .fontsize-ensurer.reset-size4.size9 {
+ font-size: 2.3em;
+}
+.katex .sizing.reset-size4.size10,
+.katex .fontsize-ensurer.reset-size4.size10 {
+ font-size: 2.76666667em;
+}
+.katex .sizing.reset-size5.size1,
+.katex .fontsize-ensurer.reset-size5.size1 {
+ font-size: 0.5em;
+}
+.katex .sizing.reset-size5.size2,
+.katex .fontsize-ensurer.reset-size5.size2 {
+ font-size: 0.7em;
+}
+.katex .sizing.reset-size5.size3,
+.katex .fontsize-ensurer.reset-size5.size3 {
+ font-size: 0.8em;
+}
+.katex .sizing.reset-size5.size4,
+.katex .fontsize-ensurer.reset-size5.size4 {
+ font-size: 0.9em;
+}
+.katex .sizing.reset-size5.size5,
+.katex .fontsize-ensurer.reset-size5.size5 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size5.size6,
+.katex .fontsize-ensurer.reset-size5.size6 {
+ font-size: 1.2em;
+}
+.katex .sizing.reset-size5.size7,
+.katex .fontsize-ensurer.reset-size5.size7 {
+ font-size: 1.44em;
+}
+.katex .sizing.reset-size5.size8,
+.katex .fontsize-ensurer.reset-size5.size8 {
+ font-size: 1.73em;
+}
+.katex .sizing.reset-size5.size9,
+.katex .fontsize-ensurer.reset-size5.size9 {
+ font-size: 2.07em;
+}
+.katex .sizing.reset-size5.size10,
+.katex .fontsize-ensurer.reset-size5.size10 {
+ font-size: 2.49em;
+}
+.katex .sizing.reset-size6.size1,
+.katex .fontsize-ensurer.reset-size6.size1 {
+ font-size: 0.41666667em;
+}
+.katex .sizing.reset-size6.size2,
+.katex .fontsize-ensurer.reset-size6.size2 {
+ font-size: 0.58333333em;
+}
+.katex .sizing.reset-size6.size3,
+.katex .fontsize-ensurer.reset-size6.size3 {
+ font-size: 0.66666667em;
+}
+.katex .sizing.reset-size6.size4,
+.katex .fontsize-ensurer.reset-size6.size4 {
+ font-size: 0.75em;
+}
+.katex .sizing.reset-size6.size5,
+.katex .fontsize-ensurer.reset-size6.size5 {
+ font-size: 0.83333333em;
+}
+.katex .sizing.reset-size6.size6,
+.katex .fontsize-ensurer.reset-size6.size6 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size6.size7,
+.katex .fontsize-ensurer.reset-size6.size7 {
+ font-size: 1.2em;
+}
+.katex .sizing.reset-size6.size8,
+.katex .fontsize-ensurer.reset-size6.size8 {
+ font-size: 1.44166667em;
+}
+.katex .sizing.reset-size6.size9,
+.katex .fontsize-ensurer.reset-size6.size9 {
+ font-size: 1.725em;
+}
+.katex .sizing.reset-size6.size10,
+.katex .fontsize-ensurer.reset-size6.size10 {
+ font-size: 2.075em;
+}
+.katex .sizing.reset-size7.size1,
+.katex .fontsize-ensurer.reset-size7.size1 {
+ font-size: 0.34722222em;
+}
+.katex .sizing.reset-size7.size2,
+.katex .fontsize-ensurer.reset-size7.size2 {
+ font-size: 0.48611111em;
+}
+.katex .sizing.reset-size7.size3,
+.katex .fontsize-ensurer.reset-size7.size3 {
+ font-size: 0.55555556em;
+}
+.katex .sizing.reset-size7.size4,
+.katex .fontsize-ensurer.reset-size7.size4 {
+ font-size: 0.625em;
+}
+.katex .sizing.reset-size7.size5,
+.katex .fontsize-ensurer.reset-size7.size5 {
+ font-size: 0.69444444em;
+}
+.katex .sizing.reset-size7.size6,
+.katex .fontsize-ensurer.reset-size7.size6 {
+ font-size: 0.83333333em;
+}
+.katex .sizing.reset-size7.size7,
+.katex .fontsize-ensurer.reset-size7.size7 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size7.size8,
+.katex .fontsize-ensurer.reset-size7.size8 {
+ font-size: 1.20138889em;
+}
+.katex .sizing.reset-size7.size9,
+.katex .fontsize-ensurer.reset-size7.size9 {
+ font-size: 1.4375em;
+}
+.katex .sizing.reset-size7.size10,
+.katex .fontsize-ensurer.reset-size7.size10 {
+ font-size: 1.72916667em;
+}
+.katex .sizing.reset-size8.size1,
+.katex .fontsize-ensurer.reset-size8.size1 {
+ font-size: 0.28901734em;
+}
+.katex .sizing.reset-size8.size2,
+.katex .fontsize-ensurer.reset-size8.size2 {
+ font-size: 0.40462428em;
+}
+.katex .sizing.reset-size8.size3,
+.katex .fontsize-ensurer.reset-size8.size3 {
+ font-size: 0.46242775em;
+}
+.katex .sizing.reset-size8.size4,
+.katex .fontsize-ensurer.reset-size8.size4 {
+ font-size: 0.52023121em;
+}
+.katex .sizing.reset-size8.size5,
+.katex .fontsize-ensurer.reset-size8.size5 {
+ font-size: 0.57803468em;
+}
+.katex .sizing.reset-size8.size6,
+.katex .fontsize-ensurer.reset-size8.size6 {
+ font-size: 0.69364162em;
+}
+.katex .sizing.reset-size8.size7,
+.katex .fontsize-ensurer.reset-size8.size7 {
+ font-size: 0.83236994em;
+}
+.katex .sizing.reset-size8.size8,
+.katex .fontsize-ensurer.reset-size8.size8 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size8.size9,
+.katex .fontsize-ensurer.reset-size8.size9 {
+ font-size: 1.19653179em;
+}
+.katex .sizing.reset-size8.size10,
+.katex .fontsize-ensurer.reset-size8.size10 {
+ font-size: 1.43930636em;
+}
+.katex .sizing.reset-size9.size1,
+.katex .fontsize-ensurer.reset-size9.size1 {
+ font-size: 0.24154589em;
+}
+.katex .sizing.reset-size9.size2,
+.katex .fontsize-ensurer.reset-size9.size2 {
+ font-size: 0.33816425em;
+}
+.katex .sizing.reset-size9.size3,
+.katex .fontsize-ensurer.reset-size9.size3 {
+ font-size: 0.38647343em;
+}
+.katex .sizing.reset-size9.size4,
+.katex .fontsize-ensurer.reset-size9.size4 {
+ font-size: 0.43478261em;
+}
+.katex .sizing.reset-size9.size5,
+.katex .fontsize-ensurer.reset-size9.size5 {
+ font-size: 0.48309179em;
+}
+.katex .sizing.reset-size9.size6,
+.katex .fontsize-ensurer.reset-size9.size6 {
+ font-size: 0.57971014em;
+}
+.katex .sizing.reset-size9.size7,
+.katex .fontsize-ensurer.reset-size9.size7 {
+ font-size: 0.69565217em;
+}
+.katex .sizing.reset-size9.size8,
+.katex .fontsize-ensurer.reset-size9.size8 {
+ font-size: 0.83574879em;
+}
+.katex .sizing.reset-size9.size9,
+.katex .fontsize-ensurer.reset-size9.size9 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size9.size10,
+.katex .fontsize-ensurer.reset-size9.size10 {
+ font-size: 1.20289855em;
+}
+.katex .sizing.reset-size10.size1,
+.katex .fontsize-ensurer.reset-size10.size1 {
+ font-size: 0.20080321em;
+}
+.katex .sizing.reset-size10.size2,
+.katex .fontsize-ensurer.reset-size10.size2 {
+ font-size: 0.2811245em;
+}
+.katex .sizing.reset-size10.size3,
+.katex .fontsize-ensurer.reset-size10.size3 {
+ font-size: 0.32128514em;
+}
+.katex .sizing.reset-size10.size4,
+.katex .fontsize-ensurer.reset-size10.size4 {
+ font-size: 0.36144578em;
+}
+.katex .sizing.reset-size10.size5,
+.katex .fontsize-ensurer.reset-size10.size5 {
+ font-size: 0.40160643em;
+}
+.katex .sizing.reset-size10.size6,
+.katex .fontsize-ensurer.reset-size10.size6 {
+ font-size: 0.48192771em;
+}
+.katex .sizing.reset-size10.size7,
+.katex .fontsize-ensurer.reset-size10.size7 {
+ font-size: 0.57831325em;
+}
+.katex .sizing.reset-size10.size8,
+.katex .fontsize-ensurer.reset-size10.size8 {
+ font-size: 0.69477912em;
+}
+.katex .sizing.reset-size10.size9,
+.katex .fontsize-ensurer.reset-size10.size9 {
+ font-size: 0.8313253em;
+}
+.katex .sizing.reset-size10.size10,
+.katex .fontsize-ensurer.reset-size10.size10 {
+ font-size: 1em;
+}
+.katex .delimsizing.size1 {
+ font-family: KaTeX_Size1;
+}
+.katex .delimsizing.size2 {
+ font-family: KaTeX_Size2;
+}
+.katex .delimsizing.size3 {
+ font-family: KaTeX_Size3;
+}
+.katex .delimsizing.size4 {
+ font-family: KaTeX_Size4;
+}
+.katex .delimsizing.mult .delim-size1 > span {
+ font-family: KaTeX_Size1;
+}
+.katex .delimsizing.mult .delim-size4 > span {
+ font-family: KaTeX_Size4;
+}
+.katex .nulldelimiter {
+ display: inline-block;
+ width: 0.12em;
+}
+.katex .op-symbol {
+ position: relative;
+}
+.katex .op-symbol.small-op {
+ font-family: KaTeX_Size1;
+}
+.katex .op-symbol.large-op {
+ font-family: KaTeX_Size2;
+}
+.katex .op-limits > .vlist > span {
+ text-align: center;
+}
+.katex .accent > .vlist > span {
+ text-align: center;
+}
+.katex .accent .accent-body > span {
+ width: 0;
+}
+.katex .accent .accent-body.accent-vec > span {
+ position: relative;
+ left: 0.326em;
+}
+.katex .mtable .vertical-separator {
+ display: inline-block;
+ margin: 0 -0.025em;
+ border-right: 0.05em solid black;
+}
+.katex .mtable .arraycolsep {
+ display: inline-block;
+}
+.katex .mtable .col-align-c > .vlist {
+ text-align: center;
+}
+.katex .mtable .col-align-l > .vlist {
+ text-align: left;
+}
+.katex .mtable .col-align-r > .vlist {
+ text-align: right;
+}